@gajae-code/coding-agent 0.18.6 → 0.18.8
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 +318 -1
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/capability/skill.d.ts +2 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/public-command-errors.d.ts +3 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/autorouting-tier-map.d.ts +348 -9
- package/dist/types/config/file-lock.d.ts +1 -0
- package/dist/types/config/model-bindings-applier.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -3
- package/dist/types/config/model-registry.d.ts +4 -9
- package/dist/types/config/settings.d.ts +4 -0
- package/dist/types/config/skill-settings-defaults.d.ts +2 -0
- package/dist/types/cursor-delete-path.d.ts +1 -0
- package/dist/types/cursor-mcp-name.d.ts +8 -0
- package/dist/types/customization/inventory.d.ts +2 -0
- package/dist/types/debug/report-bundle-parent.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
- package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
- package/dist/types/discovery/helpers.d.ts +8 -3
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
- package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
- package/dist/types/extensibility/skill-management.d.ts +3 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
- package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
- package/dist/types/internal-urls/skill-path.d.ts +1 -0
- package/dist/types/lsp/workspace-path.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
- package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
- package/dist/types/progress/collect-project-progress.d.ts +38 -0
- package/dist/types/progress/progress-contract.d.ts +154 -0
- package/dist/types/progress/progress-snapshot.d.ts +11 -0
- package/dist/types/progress/project-progress.d.ts +141 -0
- package/dist/types/progress/render-progress.d.ts +28 -0
- package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk/acp/adapter.d.ts +1 -1
- package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
- package/dist/types/sdk/broker/broker.d.ts +18 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- package/dist/types/sdk/bus/index.d.ts +12 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +6 -0
- package/dist/types/sdk/host/query/handlers.d.ts +3 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +38 -9
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
- package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +71 -0
- package/dist/types/session/session-storage.d.ts +62 -48
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -0
- package/dist/types/setup/credential-auto-import.d.ts +1 -1
- package/dist/types/setup/credential-import.d.ts +6 -4
- package/dist/types/skill-state/active-state.d.ts +38 -0
- package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/python.d.ts +9 -1
- package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
- package/dist/types/tools/restricted-cwd.d.ts +1 -0
- package/dist/types/tools/todo-write.d.ts +6 -0
- package/dist/types/utils/tools-checksum.d.ts +7 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +21 -0
- package/scripts/resident-memory-bench.ts +174 -20
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/job-manager.ts +228 -122
- package/src/capability/index.ts +18 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/config-cli.ts +8 -18
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/cli-main.ts +7 -8
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/sdk.ts +140 -29
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +307 -68
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-bindings-applier.ts +20 -0
- package/src/config/model-profile-activation.ts +240 -46
- package/src/config/model-registry.ts +47 -153
- package/src/config/settings.ts +44 -4
- package/src/config/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/cursor-delete-path.ts +10 -0
- package/src/cursor-mcp-name.ts +15 -0
- package/src/cursor.ts +13 -2
- package/src/customization/inventory.ts +3 -0
- package/src/debug/report-bundle-parent.ts +22 -0
- package/src/debug/report-bundle.ts +3 -0
- package/src/debug/system-info.ts +13 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
- package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
- package/src/defaults/gjc-defaults.test.ts +18 -1
- package/src/defaults/gjc-defaults.ts +3 -0
- package/src/defaults/gjc-skills.generated.ts +58 -36
- package/src/discovery/agents.ts +6 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +168 -23
- package/src/discovery/mcp-json.ts +6 -0
- package/src/discovery/ssh.ts +9 -4
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +380 -120
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/index.ts +5 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
- package/src/extensibility/plugins/manager.ts +3 -2
- package/src/extensibility/plugins/marketplace/cache.ts +15 -0
- package/src/extensibility/plugins/marketplace/manager.ts +11 -3
- package/src/extensibility/plugins/plugin-link-path.ts +12 -0
- package/src/extensibility/runtime-skill-discovery.ts +20 -2
- package/src/extensibility/skill-management.ts +6 -2
- package/src/extensibility/skills.ts +6 -1
- package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
- package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
- package/src/gjc-runtime/deep-interview-stage.ts +98 -21
- package/src/gjc-runtime/gc-runtime.ts +206 -8
- package/src/gjc-runtime/launch-worktree.ts +14 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/state-argv.ts +2 -0
- package/src/gjc-runtime/state-migrations.ts +91 -29
- package/src/gjc-runtime/state-runtime.ts +3716 -135
- package/src/gjc-runtime/state-writer.ts +83 -8
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
- package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
- package/src/gjc-runtime/workflow-manifest.ts +54 -21
- package/src/harness-control-plane/operate.ts +12 -4
- package/src/harness-control-plane/owner.ts +5 -1
- package/src/hashline/parser.ts +48 -9
- package/src/hashline/recovery.ts +13 -4
- package/src/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +19 -18
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/modes/acp/acp-agent.ts +163 -7
- package/src/modes/components/custom-editor.ts +14 -0
- package/src/modes/components/customization/customization-dashboard.ts +12 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -2
- package/src/modes/components/extensions/state-manager.ts +15 -5
- package/src/modes/components/model-selector.ts +1 -4
- package/src/modes/components/provider-onboarding-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +4 -0
- package/src/modes/controllers/event-controller.ts +11 -4
- package/src/modes/controllers/input-controller.ts +74 -29
- package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
- package/src/modes/controllers/selector-controller.ts +28 -15
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/runtime-init.ts +4 -0
- package/src/modes/utils/abort-message.ts +3 -1
- package/src/modes/utils/interrupt-ownership.ts +12 -0
- package/src/modes/utils/keybinding-matchers.ts +4 -0
- package/src/modes/utils/macos-option-forwarding.ts +80 -0
- package/src/progress/collect-project-progress.ts +153 -0
- package/src/progress/progress-contract.ts +172 -0
- package/src/progress/progress-snapshot.ts +140 -0
- package/src/progress/project-progress.ts +446 -0
- package/src/progress/render-progress.ts +209 -0
- package/src/prompts/system/master-mode.md +1 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/runtime-mcp/client.ts +13 -4
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/elicitation.ts +142 -0
- package/src/runtime-mcp/oauth-flow.ts +5 -0
- package/src/runtime-mcp/oauth-public-url.ts +8 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +2 -2
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +93 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +83 -7
- package/src/sdk/broker/ensure.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +820 -73
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +241 -46
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/bus/telegram-daemon.ts +4 -2
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2805 -411
- package/src/sdk/host/session-runtime.ts +428 -111
- package/src/sdk/host/surface-policy.ts +1 -0
- package/src/sdk/index.ts +8 -0
- package/src/sdk/lifecycle-session.ts +3 -1
- package/src/sdk/mcp/server.ts +15 -3
- package/src/sdk/prompt-deadline-manager.ts +15 -0
- package/src/sdk/protocol/operation-inventory.generated.json +146 -0
- package/src/sdk/protocol/operation-registry.ts +9 -2
- package/src/sdk/router/session-router.ts +94 -14
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +81 -38
- package/src/session/agent-session.ts +674 -224
- package/src/session/artifacts.ts +174 -70
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3457 -462
- package/src/session/internal/managed-session-storage.ts +482 -61
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +245 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
- package/src/session/session-manager.ts +939 -133
- package/src/session/session-retirement.ts +714 -0
- package/src/session/session-storage.ts +418 -174
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +641 -0
- package/src/session/task-artifact-owner-retirement.ts +477 -0
- package/src/session/task-artifact-owner.ts +289 -0
- package/src/setup/credential-auto-import.ts +12 -3
- package/src/setup/credential-import.ts +176 -5
- package/src/setup/paseo/announce.ts +6 -1
- package/src/skill-state/active-state.ts +132 -2
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/progress-report.ts +23 -0
- package/src/stt/setup.ts +3 -5
- package/src/task/commands.ts +3 -1
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +6 -0
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit.ts +4 -0
- package/src/tools/bash-allowed-prefixes.ts +15 -0
- package/src/tools/bash-skill-urls.ts +56 -3
- package/src/tools/bash.ts +48 -79
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/python.ts +187 -66
- package/src/tools/recipe/index.ts +9 -0
- package/src/tools/recipe/recipe-guard.ts +19 -0
- package/src/tools/restricted-cwd.ts +10 -0
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +28 -1
- package/src/tools/todo-write.ts +10 -1
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/write.ts +1 -0
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/search/providers/codex.ts +3 -2
package/src/eval/py/executor.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
checkPythonKernelAvailability,
|
|
12
12
|
type KernelExecuteOptions,
|
|
13
13
|
type KernelExecuteResult,
|
|
14
|
+
type KernelShutdownResult,
|
|
14
15
|
PythonKernel,
|
|
15
16
|
} from "./kernel";
|
|
16
17
|
import type { PythonRuntimeOptions } from "./runtime";
|
|
@@ -116,12 +117,26 @@ interface PythonSession {
|
|
|
116
117
|
ownerIds: Set<string>;
|
|
117
118
|
hasFallbackOwner: boolean;
|
|
118
119
|
queue: Promise<void>;
|
|
120
|
+
cleanupPromise?: Promise<KernelShutdownResult>;
|
|
121
|
+
cleanupStart?: () => void;
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
interface InitializingPythonSession {
|
|
122
|
-
sessionId: string;
|
|
123
125
|
promise: Promise<PythonSession>;
|
|
126
|
+
ownerIds: Set<string>;
|
|
127
|
+
retirementOwnerIds: Set<string>;
|
|
128
|
+
hasFallbackOwner: boolean;
|
|
124
129
|
cancelled?: PythonExecutionCancelledError;
|
|
130
|
+
kernel?: PythonKernel;
|
|
131
|
+
cleanupError?: unknown;
|
|
132
|
+
cleanupFailed?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface ActivePythonRequest {
|
|
136
|
+
readonly ownerId?: string;
|
|
137
|
+
readonly controller: AbortController;
|
|
138
|
+
readonly completion: Promise<void>;
|
|
139
|
+
readonly finish: () => void;
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
let pythonResourceCleanupRegistered = false;
|
|
@@ -134,6 +149,11 @@ function ensurePythonResourceCleanup(): void {
|
|
|
134
149
|
const sessions = new Map<string, PythonSession | InitializingPythonSession>();
|
|
135
150
|
const retiringKernels = new Set<PythonKernel>();
|
|
136
151
|
const settingsScopes = new WeakMap<Settings, string>();
|
|
152
|
+
const activeRequests = new Set<ActivePythonRequest>();
|
|
153
|
+
const retiringSessions = new Set<PythonSession>();
|
|
154
|
+
const initializingSessions = new Set<InitializingPythonSession>();
|
|
155
|
+
const retiringKernelShutdowns = new Map<PythonKernel, Promise<KernelShutdownResult>>();
|
|
156
|
+
const retiringKernelOwners = new Map<PythonKernel, Set<string>>();
|
|
137
157
|
|
|
138
158
|
/**
|
|
139
159
|
* Fingerprint the settings values that actually shape a spawned kernel process.
|
|
@@ -169,11 +189,101 @@ function scopedSessionId(sessionId: string, activeSettings: Settings | undefined
|
|
|
169
189
|
return `${sessionId}:settings-${settingsScope(activeSettings)}`;
|
|
170
190
|
}
|
|
171
191
|
|
|
172
|
-
|
|
173
|
-
const result = await kernel.shutdown().catch(() => undefined);
|
|
174
|
-
if (result?.confirmed) return;
|
|
192
|
+
function retainKernelForOwners(kernel: PythonKernel, ownerIds: Set<string>): void {
|
|
175
193
|
retiringKernels.add(kernel);
|
|
194
|
+
const owners = retiringKernelOwners.get(kernel) ?? new Set<string>();
|
|
195
|
+
for (const ownerId of ownerIds) owners.add(ownerId);
|
|
196
|
+
retiringKernelOwners.set(kernel, owners);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function shutdownOrRetainKernel(kernel: PythonKernel, ownerIds: Set<string>): Promise<void> {
|
|
200
|
+
let result: KernelShutdownResult;
|
|
201
|
+
try {
|
|
202
|
+
result = await callKernelShutdown(kernel);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
retainKernelForOwners(kernel, ownerIds);
|
|
205
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id, reason: error });
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
if (result.confirmed) return;
|
|
209
|
+
retainKernelForOwners(kernel, ownerIds);
|
|
176
210
|
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id });
|
|
211
|
+
throw unconfirmedShutdownError(kernel);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function prepareSessionShutdown(session: PythonSession): Promise<KernelShutdownResult> {
|
|
215
|
+
if (session.cleanupPromise) return session.cleanupPromise;
|
|
216
|
+
const { promise: cleanup, resolve, reject } = Promise.withResolvers<KernelShutdownResult>();
|
|
217
|
+
retiringSessions.add(session);
|
|
218
|
+
session.cleanupPromise = cleanup;
|
|
219
|
+
session.cleanupStart = () => void confirmedKernelShutdown(session.kernel).then(resolve, reject);
|
|
220
|
+
void cleanup.then(
|
|
221
|
+
result => {
|
|
222
|
+
if (result.confirmed) retiringSessions.delete(session);
|
|
223
|
+
if (session.cleanupPromise === cleanup) {
|
|
224
|
+
session.cleanupPromise = undefined;
|
|
225
|
+
session.cleanupStart = undefined;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
() => {
|
|
229
|
+
retiringSessions.add(session);
|
|
230
|
+
if (session.cleanupPromise === cleanup) {
|
|
231
|
+
session.cleanupPromise = undefined;
|
|
232
|
+
session.cleanupStart = undefined;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
return cleanup;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function startSessionShutdown(session: PythonSession): void {
|
|
240
|
+
const start = session.cleanupStart;
|
|
241
|
+
if (!start) return;
|
|
242
|
+
session.cleanupStart = undefined;
|
|
243
|
+
start();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function callKernelShutdown(kernel: PythonKernel): Promise<KernelShutdownResult> {
|
|
247
|
+
try {
|
|
248
|
+
return kernel.shutdown();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return Promise.reject(error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function confirmedKernelShutdown(kernel: PythonKernel): Promise<KernelShutdownResult> {
|
|
255
|
+
return callKernelShutdown(kernel).then(result => {
|
|
256
|
+
if (!result.confirmed) throw unconfirmedShutdownError(kernel);
|
|
257
|
+
return result;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function prepareRetiringKernelShutdown(kernel: PythonKernel): [Promise<KernelShutdownResult>, () => void] {
|
|
262
|
+
const existing = retiringKernelShutdowns.get(kernel);
|
|
263
|
+
if (existing) return [existing, () => {}];
|
|
264
|
+
const { promise, resolve, reject } = Promise.withResolvers<KernelShutdownResult>();
|
|
265
|
+
retiringKernelShutdowns.set(kernel, promise);
|
|
266
|
+
let started = false;
|
|
267
|
+
void promise.then(
|
|
268
|
+
result => {
|
|
269
|
+
if (result.confirmed) {
|
|
270
|
+
retiringKernels.delete(kernel);
|
|
271
|
+
retiringKernelOwners.delete(kernel);
|
|
272
|
+
}
|
|
273
|
+
if (retiringKernelShutdowns.get(kernel) === promise) retiringKernelShutdowns.delete(kernel);
|
|
274
|
+
},
|
|
275
|
+
() => {
|
|
276
|
+
if (retiringKernelShutdowns.get(kernel) === promise) retiringKernelShutdowns.delete(kernel);
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
return [
|
|
280
|
+
promise,
|
|
281
|
+
() => {
|
|
282
|
+
if (started) return;
|
|
283
|
+
started = true;
|
|
284
|
+
void confirmedKernelShutdown(kernel).then(resolve, reject);
|
|
285
|
+
},
|
|
286
|
+
];
|
|
177
287
|
}
|
|
178
288
|
|
|
179
289
|
function isInitializingSession(
|
|
@@ -202,6 +312,59 @@ function getExecutionDeadlineMs(options?: Pick<PythonExecutorOptions, "deadlineM
|
|
|
202
312
|
return Date.now() + options.timeoutMs;
|
|
203
313
|
}
|
|
204
314
|
|
|
315
|
+
function beginPythonRequest(
|
|
316
|
+
options?: PythonExecutorOptions,
|
|
317
|
+
useSessionFallback = false,
|
|
318
|
+
): {
|
|
319
|
+
options: PythonExecutorOptions;
|
|
320
|
+
request: ActivePythonRequest;
|
|
321
|
+
cwd?: string;
|
|
322
|
+
} {
|
|
323
|
+
ensurePythonResourceCleanup();
|
|
324
|
+
const capturedOptions = { ...(options ?? {}) };
|
|
325
|
+
const cwd = useSessionFallback ? (capturedOptions.cwd ?? getProjectDir()) : undefined;
|
|
326
|
+
const ownerId =
|
|
327
|
+
capturedOptions.kernelOwnerId ??
|
|
328
|
+
(useSessionFallback && capturedOptions.kernelMode !== "per-call"
|
|
329
|
+
? scopedSessionId(
|
|
330
|
+
capturedOptions.sessionId ?? `session:${cwd}`,
|
|
331
|
+
capturedOptions.settings,
|
|
332
|
+
capturedOptions.sessionId !== undefined,
|
|
333
|
+
)
|
|
334
|
+
: undefined);
|
|
335
|
+
const deadlineMs = getExecutionDeadlineMs(capturedOptions);
|
|
336
|
+
const controller = new AbortController();
|
|
337
|
+
const { promise: completion, resolve } = Promise.withResolvers<void>();
|
|
338
|
+
let deadlineTimer: NodeJS.Timeout | undefined;
|
|
339
|
+
const request: ActivePythonRequest = {
|
|
340
|
+
ownerId,
|
|
341
|
+
controller,
|
|
342
|
+
completion,
|
|
343
|
+
finish: () => {
|
|
344
|
+
activeRequests.delete(request);
|
|
345
|
+
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
346
|
+
resolve();
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
activeRequests.add(request);
|
|
350
|
+
|
|
351
|
+
const signals = [controller.signal];
|
|
352
|
+
if (capturedOptions.signal) signals.push(capturedOptions.signal);
|
|
353
|
+
const signal = AbortSignal.any(signals);
|
|
354
|
+
const remainingMs = getRemainingTimeoutMs(deadlineMs);
|
|
355
|
+
deadlineTimer =
|
|
356
|
+
remainingMs !== undefined
|
|
357
|
+
? setTimeout(() => controller.abort(new PythonExecutionCancelledError(true)), Math.max(0, remainingMs))
|
|
358
|
+
: undefined;
|
|
359
|
+
deadlineTimer?.unref?.();
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
options: { ...capturedOptions, signal, deadlineMs },
|
|
363
|
+
request,
|
|
364
|
+
cwd,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
205
368
|
function getRemainingTimeoutMs(deadlineMs?: number): number | undefined {
|
|
206
369
|
if (deadlineMs === undefined) return undefined;
|
|
207
370
|
return deadlineMs - Date.now();
|
|
@@ -233,6 +396,13 @@ function isTimedOutCancellation(error: unknown, signal?: AbortSignal): boolean {
|
|
|
233
396
|
return reason instanceof Error ? reason.name === "TimeoutError" : false;
|
|
234
397
|
}
|
|
235
398
|
|
|
399
|
+
function throwIfExecutionCancelled(options: Pick<PythonExecutorOptions, "signal" | "deadlineMs">): void {
|
|
400
|
+
if (options.signal?.aborted) {
|
|
401
|
+
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
402
|
+
}
|
|
403
|
+
requireRemainingTimeoutMs(options.deadlineMs);
|
|
404
|
+
}
|
|
405
|
+
|
|
236
406
|
async function waitForPromiseWithCancellation<T>(
|
|
237
407
|
promise: Promise<T>,
|
|
238
408
|
options: Pick<PythonExecutorOptions, "signal" | "deadlineMs">,
|
|
@@ -333,7 +503,7 @@ function buildKernelEnv(options: {
|
|
|
333
503
|
}
|
|
334
504
|
|
|
335
505
|
async function startKernel(cwd: string, options: PythonExecutorOptions): Promise<PythonKernel> {
|
|
336
|
-
|
|
506
|
+
throwIfExecutionCancelled(options);
|
|
337
507
|
return await PythonKernel.start({
|
|
338
508
|
cwd,
|
|
339
509
|
settings: options.settings,
|
|
@@ -344,7 +514,11 @@ async function startKernel(cwd: string, options: PythonExecutorOptions): Promise
|
|
|
344
514
|
});
|
|
345
515
|
}
|
|
346
516
|
|
|
347
|
-
function attachOwner(
|
|
517
|
+
function attachOwner(
|
|
518
|
+
session: PythonSession | InitializingPythonSession,
|
|
519
|
+
sessionId: string,
|
|
520
|
+
ownerId: string | undefined,
|
|
521
|
+
): void {
|
|
348
522
|
if (ownerId !== undefined) {
|
|
349
523
|
if (session.hasFallbackOwner) {
|
|
350
524
|
session.ownerIds.delete(sessionId);
|
|
@@ -362,6 +536,7 @@ function attachOwner(session: PythonSession, sessionId: string, ownerId: string
|
|
|
362
536
|
async function acquireSession(sessionId: string, cwd: string, options: PythonExecutorOptions): Promise<PythonSession> {
|
|
363
537
|
const existing = sessions.get(sessionId);
|
|
364
538
|
if (existing) {
|
|
539
|
+
attachOwner(existing, sessionId, options.kernelOwnerId);
|
|
365
540
|
let session: PythonSession;
|
|
366
541
|
if (isInitializingSession(existing)) {
|
|
367
542
|
try {
|
|
@@ -379,22 +554,37 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
379
554
|
} else {
|
|
380
555
|
session = existing;
|
|
381
556
|
}
|
|
382
|
-
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
383
|
-
ensurePythonResourceCleanup();
|
|
384
557
|
return session;
|
|
385
558
|
}
|
|
386
559
|
|
|
387
560
|
const initializing: InitializingPythonSession = {
|
|
388
|
-
|
|
561
|
+
ownerIds: new Set(),
|
|
562
|
+
retirementOwnerIds: new Set(),
|
|
563
|
+
hasFallbackOwner: false,
|
|
389
564
|
promise: Promise.resolve().then(async () => {
|
|
565
|
+
throwIfExecutionCancelled(options);
|
|
390
566
|
const kernel = await startKernel(cwd, options);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
567
|
+
initializing.kernel = kernel;
|
|
568
|
+
if (initializing.cancelled || options.signal?.aborted) {
|
|
569
|
+
try {
|
|
570
|
+
await shutdownOrRetainKernel(kernel, initializing.ownerIds);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
initializing.cleanupError = error;
|
|
573
|
+
initializing.cleanupFailed = true;
|
|
574
|
+
}
|
|
575
|
+
throw (
|
|
576
|
+
initializing.cancelled ??
|
|
577
|
+
new PythonExecutionCancelledError(isTimedOutCancellation(options.signal?.reason, options.signal))
|
|
578
|
+
);
|
|
394
579
|
}
|
|
395
580
|
const current = sessions.get(sessionId);
|
|
396
581
|
if (current !== initializing) {
|
|
397
|
-
|
|
582
|
+
try {
|
|
583
|
+
await shutdownOrRetainKernel(kernel, initializing.ownerIds);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
initializing.cleanupError = error;
|
|
586
|
+
initializing.cleanupFailed = true;
|
|
587
|
+
}
|
|
398
588
|
const winner = current
|
|
399
589
|
? isInitializingSession(current)
|
|
400
590
|
? await waitForPromiseWithCancellation(current.promise, options)
|
|
@@ -408,16 +598,17 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
408
598
|
kernel,
|
|
409
599
|
kernelInstanceId: crypto.randomUUID(),
|
|
410
600
|
bridgeCapability: options.bridge?.capability,
|
|
411
|
-
ownerIds: new Set(),
|
|
412
|
-
hasFallbackOwner:
|
|
601
|
+
ownerIds: new Set(initializing.ownerIds),
|
|
602
|
+
hasFallbackOwner: initializing.hasFallbackOwner,
|
|
413
603
|
queue: Promise.resolve(),
|
|
414
604
|
};
|
|
415
605
|
sessions.set(sessionId, session);
|
|
416
606
|
return session;
|
|
417
607
|
}),
|
|
418
608
|
};
|
|
609
|
+
attachOwner(initializing, sessionId, options.kernelOwnerId);
|
|
610
|
+
initializingSessions.add(initializing);
|
|
419
611
|
sessions.set(sessionId, initializing);
|
|
420
|
-
ensurePythonResourceCleanup();
|
|
421
612
|
let cancellationTimer: NodeJS.Timeout | undefined;
|
|
422
613
|
const retireCancelledInitialization = (timedOut: boolean): void => {
|
|
423
614
|
if (initializing.cancelled) return;
|
|
@@ -444,14 +635,13 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
444
635
|
}
|
|
445
636
|
void initializing.promise
|
|
446
637
|
.finally(() => {
|
|
638
|
+
initializingSessions.delete(initializing);
|
|
447
639
|
options.signal?.removeEventListener("abort", onAbort);
|
|
448
640
|
if (cancellationTimer) clearTimeout(cancellationTimer);
|
|
449
641
|
})
|
|
450
642
|
.catch(() => undefined);
|
|
451
643
|
try {
|
|
452
644
|
const session = await waitForPromiseWithCancellation(initializing.promise, options);
|
|
453
|
-
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
454
|
-
ensurePythonResourceCleanup();
|
|
455
645
|
return session;
|
|
456
646
|
} catch (err) {
|
|
457
647
|
if (isCancellationError(err)) {
|
|
@@ -469,22 +659,26 @@ async function replaceSessionKernel(
|
|
|
469
659
|
cwd: string,
|
|
470
660
|
options: PythonExecutorOptions,
|
|
471
661
|
): Promise<void> {
|
|
662
|
+
throwIfExecutionCancelled(options);
|
|
472
663
|
const old = session.kernel;
|
|
473
664
|
const remaining = getRemainingTimeoutMs(options.deadlineMs);
|
|
474
665
|
await old
|
|
475
666
|
.shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
|
|
476
667
|
.catch(() => undefined);
|
|
668
|
+
throwIfExecutionCancelled(options);
|
|
477
669
|
if (sessions.get(session.sessionId) !== session) {
|
|
478
670
|
throw new PythonExecutionCancelledError(false);
|
|
479
671
|
}
|
|
480
|
-
|
|
672
|
+
throwIfExecutionCancelled(options);
|
|
481
673
|
const bridge = options.bridge;
|
|
482
674
|
const previousCapability = bridge?.capability;
|
|
483
675
|
const nextCapability = bridge ? crypto.randomUUID() : undefined;
|
|
484
676
|
if (bridge && nextCapability) bridge.capability = nextCapability;
|
|
485
677
|
let next: PythonKernel | undefined;
|
|
486
678
|
try {
|
|
679
|
+
throwIfExecutionCancelled(options);
|
|
487
680
|
next = await startKernel(cwd, options);
|
|
681
|
+
throwIfExecutionCancelled(options);
|
|
488
682
|
if (sessions.get(session.sessionId) !== session) {
|
|
489
683
|
throw new PythonExecutionCancelledError(false);
|
|
490
684
|
}
|
|
@@ -503,9 +697,14 @@ async function replaceSessionKernel(
|
|
|
503
697
|
async function resetSession(sessionId: string): Promise<void> {
|
|
504
698
|
const existing = sessions.get(sessionId);
|
|
505
699
|
if (!existing) return;
|
|
506
|
-
sessions.delete(sessionId);
|
|
507
|
-
|
|
508
|
-
|
|
700
|
+
if (sessions.get(sessionId) === existing) sessions.delete(sessionId);
|
|
701
|
+
if (isInitializingSession(existing)) {
|
|
702
|
+
await existing.promise.catch(() => undefined);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const shutdown = prepareSessionShutdown(existing);
|
|
706
|
+
startSessionShutdown(existing);
|
|
707
|
+
await shutdown.catch(() => undefined);
|
|
509
708
|
}
|
|
510
709
|
|
|
511
710
|
async function runQueued<T>(
|
|
@@ -533,68 +732,125 @@ async function runQueued<T>(
|
|
|
533
732
|
// Public dispose entry points
|
|
534
733
|
// ---------------------------------------------------------------------------
|
|
535
734
|
|
|
536
|
-
export
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const resolved = await Promise.all(
|
|
544
|
-
all.map(
|
|
545
|
-
async ([id, entry]) =>
|
|
546
|
-
[id, isInitializingSession(entry) ? await entry.promise.catch(() => undefined) : entry] as const,
|
|
735
|
+
export function disposeAllKernelSessions(): Promise<void> {
|
|
736
|
+
const entries = [...sessions.entries()];
|
|
737
|
+
const requests = [...activeRequests];
|
|
738
|
+
const initializing = [...initializingSessions];
|
|
739
|
+
const targets = new Set<PythonSession>([
|
|
740
|
+
...[...entries.map(([, entry]) => entry), ...retiringSessions].filter(
|
|
741
|
+
(entry): entry is PythonSession => !isInitializingSession(entry),
|
|
547
742
|
),
|
|
548
|
-
);
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (
|
|
557
|
-
const reason = result.status === "rejected" ? result.reason : "not confirmed";
|
|
558
|
-
logger.warn("Python kernel shutdown not confirmed", { sessionId: id, reason });
|
|
559
|
-
if (!sessions.has(id)) sessions.set(id, session);
|
|
560
|
-
}
|
|
561
|
-
const retiringResults = await Promise.allSettled(retiring.map(kernel => kernel.shutdown()));
|
|
562
|
-
for (let i = 0; i < retiring.length; i += 1) {
|
|
563
|
-
const result = retiringResults[i];
|
|
564
|
-
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
565
|
-
retiringKernels.add(retiring[i]);
|
|
566
|
-
logger.warn("Python kernel shutdown not confirmed", {
|
|
567
|
-
kernelId: retiring[i].id,
|
|
568
|
-
reason: result.status === "rejected" ? result.reason : "not confirmed",
|
|
569
|
-
});
|
|
743
|
+
]);
|
|
744
|
+
const shutdowns = [...targets].map(session => [session, prepareSessionShutdown(session)] as const);
|
|
745
|
+
const retiring = [...retiringKernels].map(kernel => [kernel, ...prepareRetiringKernelShutdown(kernel)] as const);
|
|
746
|
+
for (const [id, entry] of entries) {
|
|
747
|
+
if (sessions.get(id) === entry) sessions.delete(id);
|
|
748
|
+
}
|
|
749
|
+
// Publish every exact physical cleanup future before abort listeners or shutdown callbacks can reenter disposal.
|
|
750
|
+
for (const request of requests) {
|
|
751
|
+
if (!request.controller.signal.aborted) request.controller.abort(new PythonExecutionCancelledError(false));
|
|
570
752
|
}
|
|
753
|
+
for (const [session] of shutdowns) startSessionShutdown(session);
|
|
754
|
+
for (const [, , start] of retiring) start();
|
|
755
|
+
return (async () => {
|
|
756
|
+
await Promise.all([
|
|
757
|
+
...requests.map(request => request.completion),
|
|
758
|
+
...initializing.map(entry => entry.promise.catch(() => undefined)),
|
|
759
|
+
]);
|
|
760
|
+
const failures: unknown[] = initializing.filter(entry => entry.cleanupFailed).map(entry => entry.cleanupError);
|
|
761
|
+
const results = await Promise.allSettled(shutdowns.map(([, cleanup]) => cleanup));
|
|
762
|
+
for (let index = 0; index < shutdowns.length; index += 1) {
|
|
763
|
+
const [session] = shutdowns[index];
|
|
764
|
+
const result = results[index];
|
|
765
|
+
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
766
|
+
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, session);
|
|
767
|
+
if (result.status === "rejected") failures.push(result.reason);
|
|
768
|
+
else failures.push(unconfirmedShutdownError(session.kernel));
|
|
769
|
+
}
|
|
770
|
+
const retiringResults = await Promise.allSettled(retiring.map(([, cleanup]) => cleanup));
|
|
771
|
+
for (let index = 0; index < retiring.length; index += 1) {
|
|
772
|
+
const [kernel] = retiring[index];
|
|
773
|
+
const result = retiringResults[index];
|
|
774
|
+
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
775
|
+
retiringKernels.add(kernel);
|
|
776
|
+
failures.push(result.status === "rejected" ? result.reason : unconfirmedShutdownError(kernel));
|
|
777
|
+
}
|
|
778
|
+
if (failures.length) throw failures[0];
|
|
779
|
+
})();
|
|
571
780
|
}
|
|
572
781
|
|
|
573
|
-
export
|
|
782
|
+
export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
783
|
+
const requests = [...activeRequests].filter(request => request.ownerId === ownerId);
|
|
784
|
+
const entries = [...sessions.entries()].filter(([, entry]) => entry.ownerIds.has(ownerId));
|
|
785
|
+
const initializing = [...initializingSessions].filter(
|
|
786
|
+
entry => entry.ownerIds.has(ownerId) || entry.retirementOwnerIds.has(ownerId),
|
|
787
|
+
);
|
|
788
|
+
const sessionsToRetire = new Set<PythonSession>([
|
|
789
|
+
...[...entries.map(([, entry]) => entry), ...retiringSessions].filter(
|
|
790
|
+
(entry): entry is PythonSession => !isInitializingSession(entry) && entry.ownerIds.has(ownerId),
|
|
791
|
+
),
|
|
792
|
+
]);
|
|
574
793
|
const toShutdown: PythonSession[] = [];
|
|
575
|
-
for (const
|
|
576
|
-
|
|
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);
|
|
798
|
+
for (const session of sessionsToRetire) {
|
|
577
799
|
if (session.ownerIds.size === 1) {
|
|
578
800
|
toShutdown.push(session);
|
|
579
|
-
|
|
801
|
+
if (sessions.get(session.sessionId) === session) sessions.delete(session.sessionId);
|
|
802
|
+
} else {
|
|
803
|
+
session.ownerIds.delete(ownerId);
|
|
580
804
|
}
|
|
581
|
-
session.ownerIds.delete(ownerId);
|
|
582
805
|
}
|
|
583
|
-
|
|
584
|
-
|
|
806
|
+
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);
|
|
810
|
+
// All cleanup futures are visible before an abort listener can reenter this API.
|
|
811
|
+
for (const request of requests) {
|
|
812
|
+
if (!request.controller.signal.aborted) request.controller.abort(new PythonExecutionCancelledError(false));
|
|
585
813
|
}
|
|
586
|
-
const
|
|
587
|
-
for (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
814
|
+
for (const [session] of shutdowns) startSessionShutdown(session);
|
|
815
|
+
for (const [, , start] of retiring) start();
|
|
816
|
+
return (async () => {
|
|
817
|
+
await Promise.all([
|
|
818
|
+
...requests.map(request => request.completion),
|
|
819
|
+
...initializing.map(entry => entry.promise.catch(() => undefined)),
|
|
820
|
+
]);
|
|
821
|
+
const failures: unknown[] = [];
|
|
822
|
+
for (const entry of initializing)
|
|
823
|
+
if (entry.cleanupFailed) {
|
|
824
|
+
if (entry.kernel) retainKernelForOwners(entry.kernel, new Set([ownerId]));
|
|
825
|
+
failures.push(entry.cleanupError);
|
|
826
|
+
}
|
|
827
|
+
const results = await Promise.allSettled(shutdowns.map(([, shutdown]) => shutdown));
|
|
828
|
+
for (let index = 0; index < shutdowns.length; index += 1) {
|
|
829
|
+
const [session] = shutdowns[index];
|
|
830
|
+
const result = results[index];
|
|
831
|
+
if (result.status === "fulfilled" && result.value.confirmed) {
|
|
832
|
+
session.ownerIds.delete(ownerId);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, session);
|
|
836
|
+
if (result.status === "rejected") failures.push(result.reason);
|
|
837
|
+
else failures.push(unconfirmedShutdownError(session.kernel));
|
|
593
838
|
}
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
839
|
+
const retiringResults = await Promise.allSettled(retiring.map(([, cleanup]) => cleanup));
|
|
840
|
+
for (let index = 0; index < retiring.length; index += 1) {
|
|
841
|
+
const [kernel] = retiring[index];
|
|
842
|
+
const result = retiringResults[index];
|
|
843
|
+
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
844
|
+
failures.push(result.status === "rejected" ? result.reason : unconfirmedShutdownError(kernel));
|
|
845
|
+
}
|
|
846
|
+
if (failures.length) throw failures[0];
|
|
847
|
+
})();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function unconfirmedShutdownError(kernel: PythonKernel): Error {
|
|
851
|
+
const error = new Error(`Python kernel shutdown not confirmed: ${kernel.id}`);
|
|
852
|
+
error.name = "PythonKernelShutdownUnconfirmedError";
|
|
853
|
+
return error;
|
|
598
854
|
}
|
|
599
855
|
|
|
600
856
|
// ---------------------------------------------------------------------------
|
|
@@ -607,6 +863,7 @@ async function executeWithKernel(
|
|
|
607
863
|
options: PythonExecutorOptions | undefined,
|
|
608
864
|
): Promise<PythonResult> {
|
|
609
865
|
const settings = options?.settings ?? (await Settings.init());
|
|
866
|
+
if (options) throwIfExecutionCancelled(options);
|
|
610
867
|
const sink = new OutputSink({
|
|
611
868
|
onChunk: options?.onChunk,
|
|
612
869
|
artifactPath: options?.artifactPath,
|
|
@@ -623,16 +880,19 @@ async function executeWithKernel(
|
|
|
623
880
|
((event: JsStatusEvent) => {
|
|
624
881
|
displayOutputs.push({ type: "status", event });
|
|
625
882
|
});
|
|
626
|
-
|
|
627
|
-
options?.toolSession && options?.bridgeSessionId && options.bridge
|
|
628
|
-
? registerPyToolBridge(options.bridgeSessionId, options.bridge.capability, {
|
|
629
|
-
toolSession: options.toolSession,
|
|
630
|
-
signal: options.signal,
|
|
631
|
-
emitStatus,
|
|
632
|
-
})
|
|
633
|
-
: null;
|
|
883
|
+
let unregisterBridge: (() => void) | null = null;
|
|
634
884
|
|
|
635
885
|
try {
|
|
886
|
+
if (options) throwIfExecutionCancelled(options);
|
|
887
|
+
unregisterBridge =
|
|
888
|
+
options?.toolSession && options?.bridgeSessionId && options.bridge
|
|
889
|
+
? registerPyToolBridge(options.bridgeSessionId, options.bridge.capability, {
|
|
890
|
+
toolSession: options.toolSession,
|
|
891
|
+
signal: options.signal,
|
|
892
|
+
emitStatus,
|
|
893
|
+
})
|
|
894
|
+
: null;
|
|
895
|
+
if (options) throwIfExecutionCancelled(options);
|
|
636
896
|
executionTimeoutMs = requireRemainingTimeoutMs(deadlineMs);
|
|
637
897
|
const result = await kernel.execute(code, {
|
|
638
898
|
signal: options?.signal,
|
|
@@ -640,6 +900,7 @@ async function executeWithKernel(
|
|
|
640
900
|
onChunk: text => sink.push(text),
|
|
641
901
|
onDisplay: output => void displayOutputs.push(output),
|
|
642
902
|
});
|
|
903
|
+
if (options) throwIfExecutionCancelled(options);
|
|
643
904
|
|
|
644
905
|
if (result.cancelled) {
|
|
645
906
|
// Prefer the caller-configured timeout for the user-facing annotation.
|
|
@@ -714,6 +975,7 @@ async function executeWithKernel(
|
|
|
714
975
|
}
|
|
715
976
|
|
|
716
977
|
async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions): Promise<void> {
|
|
978
|
+
throwIfExecutionCancelled(options);
|
|
717
979
|
const availability = await checkPythonKernelAvailability(
|
|
718
980
|
cwd,
|
|
719
981
|
options.runtimeOptions,
|
|
@@ -723,6 +985,7 @@ async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions
|
|
|
723
985
|
},
|
|
724
986
|
options.settings,
|
|
725
987
|
);
|
|
988
|
+
throwIfExecutionCancelled(options);
|
|
726
989
|
if (!availability.ok) {
|
|
727
990
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
728
991
|
}
|
|
@@ -730,10 +993,13 @@ async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions
|
|
|
730
993
|
|
|
731
994
|
async function ensureToolBridge(options: PythonExecutorOptions): Promise<void> {
|
|
732
995
|
if (!options.toolSession || options.bridge) return;
|
|
996
|
+
throwIfExecutionCancelled(options);
|
|
733
997
|
try {
|
|
734
998
|
const bridge = await ensurePyToolBridge();
|
|
999
|
+
throwIfExecutionCancelled(options);
|
|
735
1000
|
options.bridge = { ...bridge, capability: crypto.randomUUID() };
|
|
736
1001
|
} catch (err) {
|
|
1002
|
+
if (isCancellationError(err) || options.signal?.aborted) throw err;
|
|
737
1003
|
logger.warn("Failed to start Python tool bridge", {
|
|
738
1004
|
error: err instanceof Error ? err.message : String(err),
|
|
739
1005
|
});
|
|
@@ -741,11 +1007,13 @@ async function ensureToolBridge(options: PythonExecutorOptions): Promise<void> {
|
|
|
741
1007
|
}
|
|
742
1008
|
|
|
743
1009
|
async function executePerCall(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
|
|
1010
|
+
throwIfExecutionCancelled(options);
|
|
744
1011
|
if (options.bridge && !options.bridgeSessionId) {
|
|
745
1012
|
options.bridgeSessionId = `py-bridge:${crypto.randomUUID()}`;
|
|
746
1013
|
}
|
|
747
1014
|
const kernel = await startKernel(cwd, options);
|
|
748
1015
|
try {
|
|
1016
|
+
throwIfExecutionCancelled(options);
|
|
749
1017
|
return await executeWithKernel(kernel, code, options);
|
|
750
1018
|
} finally {
|
|
751
1019
|
await kernel.shutdown().catch(() => undefined);
|
|
@@ -753,6 +1021,7 @@ async function executePerCall(code: string, cwd: string, options: PythonExecutor
|
|
|
753
1021
|
}
|
|
754
1022
|
|
|
755
1023
|
async function executeOnSession(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
|
|
1024
|
+
throwIfExecutionCancelled(options);
|
|
756
1025
|
const sessionId = scopedSessionId(
|
|
757
1026
|
options.sessionId ?? `session:${cwd}`,
|
|
758
1027
|
options.settings,
|
|
@@ -762,28 +1031,32 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut
|
|
|
762
1031
|
options.bridgeSessionId = sessionId;
|
|
763
1032
|
}
|
|
764
1033
|
if (options.reset) {
|
|
1034
|
+
throwIfExecutionCancelled(options);
|
|
765
1035
|
await resetSession(sessionId);
|
|
1036
|
+
throwIfExecutionCancelled(options);
|
|
766
1037
|
}
|
|
767
1038
|
const session = await acquireSession(sessionId, cwd, options);
|
|
1039
|
+
throwIfExecutionCancelled(options);
|
|
768
1040
|
if (options.bridge && session.bridgeCapability) {
|
|
769
1041
|
options.bridge.capability = session.bridgeCapability;
|
|
770
1042
|
}
|
|
771
1043
|
options.onKernelStart?.(session.kernelInstanceId);
|
|
772
1044
|
return await runQueued(session, options, async () => {
|
|
773
|
-
|
|
774
|
-
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
775
|
-
}
|
|
1045
|
+
throwIfExecutionCancelled(options);
|
|
776
1046
|
if (sessions.get(session.sessionId) !== session) {
|
|
777
1047
|
throw new PythonExecutionCancelledError(false);
|
|
778
1048
|
}
|
|
779
1049
|
if (!session.kernel.isAlive()) {
|
|
1050
|
+
throwIfExecutionCancelled(options);
|
|
780
1051
|
await replaceSessionKernel(session, cwd, options);
|
|
1052
|
+
throwIfExecutionCancelled(options);
|
|
781
1053
|
if (sessions.get(session.sessionId) !== session) {
|
|
782
1054
|
throw new PythonExecutionCancelledError(false);
|
|
783
1055
|
}
|
|
784
1056
|
options.onKernelStart?.(session.kernelInstanceId);
|
|
785
1057
|
}
|
|
786
1058
|
try {
|
|
1059
|
+
throwIfExecutionCancelled(options);
|
|
787
1060
|
return await executeWithKernel(session.kernel, code, options);
|
|
788
1061
|
} catch (err) {
|
|
789
1062
|
if (isCancellationError(err) || options.signal?.aborted) throw err;
|
|
@@ -792,7 +1065,9 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut
|
|
|
792
1065
|
throw new PythonExecutionCancelledError(false);
|
|
793
1066
|
}
|
|
794
1067
|
// Kernel died during execute. Replace it and retry once on a fresh one.
|
|
1068
|
+
throwIfExecutionCancelled(options);
|
|
795
1069
|
await replaceSessionKernel(session, cwd, options);
|
|
1070
|
+
throwIfExecutionCancelled(options);
|
|
796
1071
|
if (sessions.get(session.sessionId) !== session) {
|
|
797
1072
|
throw new PythonExecutionCancelledError(false);
|
|
798
1073
|
}
|
|
@@ -807,43 +1082,32 @@ export async function executePythonWithKernel(
|
|
|
807
1082
|
code: string,
|
|
808
1083
|
options?: PythonExecutorOptions,
|
|
809
1084
|
): Promise<PythonResult> {
|
|
810
|
-
|
|
1085
|
+
const tracked = beginPythonRequest(options);
|
|
1086
|
+
try {
|
|
1087
|
+
return await executeWithKernel(kernel, code, tracked.options);
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
if (isCancellationError(err) || tracked.options.signal?.aborted) {
|
|
1090
|
+
return createCancelledPythonResult(
|
|
1091
|
+
isTimedOutCancellation(err, tracked.options.signal),
|
|
1092
|
+
tracked.options.timeoutMs,
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
throw err;
|
|
1096
|
+
} finally {
|
|
1097
|
+
tracked.request.finish();
|
|
1098
|
+
}
|
|
811
1099
|
}
|
|
812
1100
|
|
|
813
1101
|
export async function executePython(code: string, options?: PythonExecutorOptions): Promise<PythonResult> {
|
|
814
|
-
const
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
const combinedController = deadlineController && options?.signal ? new AbortController() : undefined;
|
|
818
|
-
const forwardAbort = (): void => combinedController?.abort(options?.signal?.reason);
|
|
819
|
-
if (combinedController) {
|
|
820
|
-
if (options?.signal?.aborted) forwardAbort();
|
|
821
|
-
else options?.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
822
|
-
}
|
|
823
|
-
const forwardDeadlineAbort = (): void => combinedController?.abort(deadlineController?.signal.reason);
|
|
824
|
-
if (combinedController && deadlineController)
|
|
825
|
-
deadlineController.signal.addEventListener("abort", forwardDeadlineAbort, { once: true });
|
|
826
|
-
const signal = combinedController?.signal ?? deadlineController?.signal ?? options?.signal;
|
|
827
|
-
const remainingMs = getRemainingTimeoutMs(deadlineMs);
|
|
828
|
-
const deadlineTimer =
|
|
829
|
-
deadlineController && remainingMs !== undefined
|
|
830
|
-
? setTimeout(() => deadlineController.abort(new PythonExecutionCancelledError(true)), Math.max(0, remainingMs))
|
|
831
|
-
: undefined;
|
|
832
|
-
deadlineTimer?.unref();
|
|
833
|
-
const executionOptions: PythonExecutorOptions = {
|
|
834
|
-
...(options ?? {}),
|
|
835
|
-
signal,
|
|
836
|
-
deadlineMs,
|
|
837
|
-
};
|
|
1102
|
+
const tracked = beginPythonRequest(options, true);
|
|
1103
|
+
const executionOptions = tracked.options;
|
|
1104
|
+
const cwd = tracked.cwd!;
|
|
838
1105
|
try {
|
|
839
|
-
|
|
840
|
-
if (executionOptions.signal?.aborted) {
|
|
841
|
-
throw new PythonExecutionCancelledError(
|
|
842
|
-
isTimedOutCancellation(executionOptions.signal.reason, executionOptions.signal),
|
|
843
|
-
);
|
|
844
|
-
}
|
|
1106
|
+
throwIfExecutionCancelled(executionOptions);
|
|
845
1107
|
await ensureKernelAvailable(cwd, executionOptions);
|
|
1108
|
+
throwIfExecutionCancelled(executionOptions);
|
|
846
1109
|
await ensureToolBridge(executionOptions);
|
|
1110
|
+
throwIfExecutionCancelled(executionOptions);
|
|
847
1111
|
|
|
848
1112
|
const kernelMode = executionOptions.kernelMode ?? "session";
|
|
849
1113
|
if (kernelMode === "per-call") {
|
|
@@ -856,10 +1120,6 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
856
1120
|
}
|
|
857
1121
|
throw err;
|
|
858
1122
|
} finally {
|
|
859
|
-
|
|
860
|
-
if (combinedController) {
|
|
861
|
-
options?.signal?.removeEventListener("abort", forwardAbort);
|
|
862
|
-
deadlineController?.signal.removeEventListener("abort", forwardDeadlineAbort);
|
|
863
|
-
}
|
|
1123
|
+
tracked.request.finish();
|
|
864
1124
|
}
|
|
865
1125
|
}
|