@gajae-code/coding-agent 0.18.7 → 0.18.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +377 -1
- package/dist/types/async/endpoint-id.d.ts +16 -0
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/async/support.d.ts +0 -14
- 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 +2 -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/commands/mcp-serve.d.ts +4 -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/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 +6 -1
- package/dist/types/discovery/mcp-json.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +18 -0
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/html-export-destination.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
- 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 +4 -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/psmux-detect.d.ts +3 -3
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
- 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 +14 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
- 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/config.d.ts +8 -5
- 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/manager.d.ts +2 -1
- package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +27 -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 +24 -2
- package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -1
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- 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/client/client.d.ts +2 -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 +2 -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 +26 -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 +2 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +31 -7
- package/dist/types/session/session-storage.d.ts +69 -2
- package/dist/types/session/task-artifact-owner-codec.d.ts +2 -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/skill-state/workflow-mutation-guard.d.ts +3 -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/task/isolation-dir.d.ts +26 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/browser/cdp-url.d.ts +2 -0
- package/dist/types/tools/github-side-effect.d.ts +22 -0
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/monitor-mutation.d.ts +9 -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/verify-managed-empty-stop.ts +28 -0
- package/src/async/endpoint-id.ts +26 -0
- package/src/async/job-manager.ts +247 -132
- package/src/async/support.ts +0 -25
- package/src/capability/index.ts +15 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +2 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/doctor/repairs.ts +4 -1
- package/src/cli/doctor-supervisor.ts +3 -0
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/public-command-registry.ts +28 -6
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/mcp-serve.ts +7 -0
- package/src/commands/sdk.ts +455 -397
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +316 -79
- 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/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/coordinator-mcp/server.ts +2 -0
- 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 +4 -13
- package/src/dap/config.ts +2 -2
- 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/register.ts +10 -6
- 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 +13 -1
- package/src/discovery/helpers.ts +171 -29
- package/src/discovery/mcp-json.ts +26 -12
- package/src/discovery/ssh.ts +1 -1
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +796 -154
- package/src/eval/py/kernel.ts +79 -13
- package/src/eval/py/tool-bridge.ts +4 -0
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/html-export-destination.ts +16 -0
- package/src/export/html/index.ts +7 -10
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
- package/src/extensibility/extensions/runner.ts +15 -1
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -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 +9 -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 -9
- package/src/gjc-runtime/launch-worktree.ts +70 -9
- package/src/gjc-runtime/psmux-detect.ts +28 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/session-state-sidecar.ts +3 -2
- package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
- package/src/gjc-runtime/state-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 +105 -16
- package/src/gjc-runtime/tmux-common.ts +8 -5
- 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/internal-urls/docs-index.generated.ts +20 -19
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/config.ts +12 -63
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/memories/index.ts +88 -7
- package/src/modes/acp/acp-agent.ts +88 -4
- 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/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +1 -1
- 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 +76 -31
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
- package/src/modes/controllers/selector-controller.ts +24 -15
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -1
- 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 +4 -2
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/json-rpc.ts +2 -1
- package/src/runtime-mcp/manager.ts +29 -11
- package/src/runtime-mcp/mcp-redirect.ts +73 -0
- package/src/runtime-mcp/oauth-flow.ts +14 -2
- package/src/runtime-mcp/oauth-public-url.ts +14 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/pool.ts +6 -0
- package/src/runtime-mcp/project-host-values.ts +46 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +4 -3
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +95 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +81 -6
- package/src/sdk/broker/doctor-restart.ts +4 -0
- package/src/sdk/broker/ensure.ts +31 -2
- package/src/sdk/broker/lifecycle.ts +711 -66
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +238 -43
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2705 -303
- package/src/sdk/host/session-runtime.ts +490 -143
- 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/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 +82 -18
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +69 -45
- package/src/session/agent-session.ts +679 -238
- package/src/session/artifacts.ts +176 -71
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3779 -462
- package/src/session/internal/managed-session-storage.ts +501 -78
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +19 -3
- package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
- package/src/session/session-manager.ts +951 -133
- package/src/session/session-retirement.ts +573 -14
- package/src/session/session-storage.ts +447 -15
- package/src/session/task-artifact-owner-codec.ts +71 -8
- package/src/session/task-artifact-owner-retirement.ts +21 -19
- 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 +145 -2
- package/src/skill-state/workflow-mutation-guard.ts +102 -10
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/mcp.ts +8 -4
- 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/task/isolation-dir.ts +143 -0
- package/src/task/worktree.ts +16 -7
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit-write-path.ts +68 -0
- package/src/tools/ast-edit.ts +10 -1
- 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/browser/cdp-url.ts +18 -0
- package/src/tools/browser.ts +8 -1
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/gh.ts +10 -0
- package/src/tools/github-side-effect.ts +112 -0
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/monitor-mutation.ts +27 -0
- package/src/tools/monitor.ts +7 -0
- package/src/tools/python.ts +188 -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/external-editor.ts +3 -2
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/insane/url-guard.ts +2 -1
- package/src/web/search/providers/codex.ts +3 -2
package/src/eval/py/executor.ts
CHANGED
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
checkPythonKernelAvailability,
|
|
12
12
|
type KernelExecuteOptions,
|
|
13
13
|
type KernelExecuteResult,
|
|
14
|
+
type KernelShutdownResult,
|
|
14
15
|
PythonKernel,
|
|
16
|
+
PythonKernelStartError,
|
|
15
17
|
} from "./kernel";
|
|
16
18
|
import type { PythonRuntimeOptions } from "./runtime";
|
|
17
19
|
import { ensurePyToolBridge, registerPyToolBridge } from "./tool-bridge";
|
|
@@ -114,14 +116,42 @@ interface PythonSession {
|
|
|
114
116
|
kernelInstanceId: string;
|
|
115
117
|
bridgeCapability?: string;
|
|
116
118
|
ownerIds: Set<string>;
|
|
119
|
+
durableOwnerIds: Set<string>;
|
|
120
|
+
provisionalOwnerCounts: Map<string, number>;
|
|
117
121
|
hasFallbackOwner: boolean;
|
|
118
122
|
queue: Promise<void>;
|
|
123
|
+
cleanupPromise?: Promise<KernelShutdownResult>;
|
|
124
|
+
cleanupStart?: () => void;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
interface InitializingPythonSession {
|
|
122
128
|
sessionId: string;
|
|
123
129
|
promise: Promise<PythonSession>;
|
|
130
|
+
startupController: AbortController;
|
|
131
|
+
requests: Set<ActivePythonRequest>;
|
|
132
|
+
ownerIds: Set<string>;
|
|
133
|
+
retirementOwnerIds: Set<string>;
|
|
134
|
+
hasFallbackOwner: boolean;
|
|
124
135
|
cancelled?: PythonExecutionCancelledError;
|
|
136
|
+
kernel?: PythonKernel;
|
|
137
|
+
cleanupAttempt?: Promise<KernelShutdownResult>;
|
|
138
|
+
cleanupError?: unknown;
|
|
139
|
+
cleanupFailed?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface ActivePythonRequest {
|
|
143
|
+
readonly ownerId?: string;
|
|
144
|
+
readonly sessionId?: string;
|
|
145
|
+
readonly fallbackOwner: boolean;
|
|
146
|
+
readonly controller: AbortController;
|
|
147
|
+
signal?: AbortSignal;
|
|
148
|
+
initializer?: InitializingPythonSession;
|
|
149
|
+
initializerAcquired?: boolean;
|
|
150
|
+
provisionalSession?: PythonSession;
|
|
151
|
+
readonly completion: Promise<void>;
|
|
152
|
+
readonly kernels: Set<PythonKernel>;
|
|
153
|
+
readonly cleanupAttempts: Map<PythonKernel, Promise<KernelShutdownResult>>;
|
|
154
|
+
readonly finish: () => void;
|
|
125
155
|
}
|
|
126
156
|
|
|
127
157
|
let pythonResourceCleanupRegistered = false;
|
|
@@ -134,6 +164,11 @@ function ensurePythonResourceCleanup(): void {
|
|
|
134
164
|
const sessions = new Map<string, PythonSession | InitializingPythonSession>();
|
|
135
165
|
const retiringKernels = new Set<PythonKernel>();
|
|
136
166
|
const settingsScopes = new WeakMap<Settings, string>();
|
|
167
|
+
const activeRequests = new Set<ActivePythonRequest>();
|
|
168
|
+
const retiringSessions = new Set<PythonSession>();
|
|
169
|
+
const initializingSessions = new Set<InitializingPythonSession>();
|
|
170
|
+
const retiringKernelShutdowns = new Map<PythonKernel, Promise<KernelShutdownResult>>();
|
|
171
|
+
const retiringKernelOwners = new Map<PythonKernel, Set<string>>();
|
|
137
172
|
|
|
138
173
|
/**
|
|
139
174
|
* Fingerprint the settings values that actually shape a spawned kernel process.
|
|
@@ -169,11 +204,118 @@ function scopedSessionId(sessionId: string, activeSettings: Settings | undefined
|
|
|
169
204
|
return `${sessionId}:settings-${settingsScope(activeSettings)}`;
|
|
170
205
|
}
|
|
171
206
|
|
|
172
|
-
|
|
173
|
-
const result = await kernel.shutdown().catch(() => undefined);
|
|
174
|
-
if (result?.confirmed) return;
|
|
207
|
+
function retainKernelForOwners(kernel: PythonKernel, ownerIds: Set<string>): void {
|
|
175
208
|
retiringKernels.add(kernel);
|
|
176
|
-
|
|
209
|
+
const owners = retiringKernelOwners.get(kernel) ?? new Set<string>();
|
|
210
|
+
for (const ownerId of ownerIds) owners.add(ownerId);
|
|
211
|
+
retiringKernelOwners.set(kernel, owners);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function retainKernelForOwner(kernel: PythonKernel | undefined, ownerId: string): void {
|
|
215
|
+
if (!kernel) return;
|
|
216
|
+
retiringKernels.add(kernel);
|
|
217
|
+
const owners = retiringKernelOwners.get(kernel) ?? new Set<string>();
|
|
218
|
+
owners.add(ownerId);
|
|
219
|
+
retiringKernelOwners.set(kernel, owners);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function removeKernelOwner(kernel: PythonKernel | undefined, ownerId: string): void {
|
|
223
|
+
if (kernel) retiringKernelOwners.get(kernel)?.delete(ownerId);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function shutdownOrRetainKernel(kernel: PythonKernel, ownerIds: Set<string>, timeoutMs?: number): Promise<void> {
|
|
227
|
+
retainKernelForOwners(kernel, ownerIds);
|
|
228
|
+
const [shutdown, start] = prepareRetiringKernelShutdown(kernel, timeoutMs);
|
|
229
|
+
start();
|
|
230
|
+
try {
|
|
231
|
+
await shutdown;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id, reason: error });
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function prepareSessionShutdown(session: PythonSession): Promise<KernelShutdownResult> {
|
|
239
|
+
if (session.cleanupPromise) return session.cleanupPromise;
|
|
240
|
+
const { promise: cleanup, resolve, reject } = Promise.withResolvers<KernelShutdownResult>();
|
|
241
|
+
retiringSessions.add(session);
|
|
242
|
+
session.cleanupPromise = cleanup;
|
|
243
|
+
retainKernelForOwners(session.kernel, session.ownerIds);
|
|
244
|
+
const [kernelCleanup, startKernelCleanup] = prepareRetiringKernelShutdown(session.kernel);
|
|
245
|
+
session.cleanupStart = () => {
|
|
246
|
+
startKernelCleanup();
|
|
247
|
+
void kernelCleanup.then(resolve, reject);
|
|
248
|
+
};
|
|
249
|
+
void cleanup.then(
|
|
250
|
+
result => {
|
|
251
|
+
if (result.confirmed) retiringSessions.delete(session);
|
|
252
|
+
if (session.cleanupPromise === cleanup) {
|
|
253
|
+
session.cleanupPromise = undefined;
|
|
254
|
+
session.cleanupStart = undefined;
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
() => {
|
|
258
|
+
retiringSessions.add(session);
|
|
259
|
+
if (session.cleanupPromise === cleanup) {
|
|
260
|
+
session.cleanupPromise = undefined;
|
|
261
|
+
session.cleanupStart = undefined;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
return cleanup;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function startSessionShutdown(session: PythonSession): void {
|
|
269
|
+
const start = session.cleanupStart;
|
|
270
|
+
if (!start) return;
|
|
271
|
+
session.cleanupStart = undefined;
|
|
272
|
+
start();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function callKernelShutdown(kernel: PythonKernel, timeoutMs?: number): Promise<KernelShutdownResult> {
|
|
276
|
+
try {
|
|
277
|
+
return kernel.shutdown(timeoutMs === undefined ? undefined : { timeoutMs });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return Promise.reject(error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function confirmedKernelShutdown(kernel: PythonKernel, timeoutMs?: number): Promise<KernelShutdownResult> {
|
|
284
|
+
return callKernelShutdown(kernel, timeoutMs).then(result => {
|
|
285
|
+
if (!result.confirmed) throw unconfirmedShutdownError(kernel);
|
|
286
|
+
return result;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function prepareRetiringKernelShutdown(
|
|
291
|
+
kernel: PythonKernel,
|
|
292
|
+
timeoutMs?: number,
|
|
293
|
+
): [Promise<KernelShutdownResult>, () => void] {
|
|
294
|
+
const existing = retiringKernelShutdowns.get(kernel);
|
|
295
|
+
if (existing) return [existing, () => {}];
|
|
296
|
+
const { promise, resolve, reject } = Promise.withResolvers<KernelShutdownResult>();
|
|
297
|
+
retiringKernelShutdowns.set(kernel, promise);
|
|
298
|
+
let started = false;
|
|
299
|
+
void promise.then(
|
|
300
|
+
result => {
|
|
301
|
+
if (result.confirmed) {
|
|
302
|
+
retiringKernels.delete(kernel);
|
|
303
|
+
retiringKernelOwners.delete(kernel);
|
|
304
|
+
}
|
|
305
|
+
if (retiringKernelShutdowns.get(kernel) === promise) retiringKernelShutdowns.delete(kernel);
|
|
306
|
+
},
|
|
307
|
+
() => {
|
|
308
|
+
if (retiringKernelShutdowns.get(kernel) === promise) retiringKernelShutdowns.delete(kernel);
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
return [
|
|
312
|
+
promise,
|
|
313
|
+
() => {
|
|
314
|
+
if (started) return;
|
|
315
|
+
started = true;
|
|
316
|
+
void confirmedKernelShutdown(kernel, timeoutMs).then(resolve, reject);
|
|
317
|
+
},
|
|
318
|
+
];
|
|
177
319
|
}
|
|
178
320
|
|
|
179
321
|
function isInitializingSession(
|
|
@@ -196,12 +338,86 @@ class PythonExecutionCancelledError extends Error {
|
|
|
196
338
|
}
|
|
197
339
|
}
|
|
198
340
|
|
|
341
|
+
class PythonKernelCleanupError extends Error {
|
|
342
|
+
constructor(
|
|
343
|
+
kernel: PythonKernel,
|
|
344
|
+
readonly cleanupError: unknown,
|
|
345
|
+
) {
|
|
346
|
+
super(`Python kernel cleanup failed: ${kernel.id}`, { cause: cleanupError });
|
|
347
|
+
this.name = "PythonKernelCleanupError";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
199
351
|
function getExecutionDeadlineMs(options?: Pick<PythonExecutorOptions, "deadlineMs" | "timeoutMs">): number | undefined {
|
|
200
352
|
if (options?.deadlineMs !== undefined) return options.deadlineMs;
|
|
201
353
|
if (options?.timeoutMs === undefined) return undefined;
|
|
202
354
|
return Date.now() + options.timeoutMs;
|
|
203
355
|
}
|
|
204
356
|
|
|
357
|
+
function beginPythonRequest(
|
|
358
|
+
options?: PythonExecutorOptions,
|
|
359
|
+
useSessionFallback = false,
|
|
360
|
+
): {
|
|
361
|
+
options: PythonExecutorOptions;
|
|
362
|
+
request: ActivePythonRequest;
|
|
363
|
+
cwd?: string;
|
|
364
|
+
} {
|
|
365
|
+
ensurePythonResourceCleanup();
|
|
366
|
+
const capturedOptions = { ...(options ?? {}) };
|
|
367
|
+
const cwd = useSessionFallback ? (capturedOptions.cwd ?? getProjectDir()) : undefined;
|
|
368
|
+
const sessionId =
|
|
369
|
+
useSessionFallback && capturedOptions.kernelMode !== "per-call"
|
|
370
|
+
? scopedSessionId(
|
|
371
|
+
capturedOptions.sessionId ?? `session:${cwd}`,
|
|
372
|
+
capturedOptions.settings,
|
|
373
|
+
capturedOptions.sessionId !== undefined,
|
|
374
|
+
)
|
|
375
|
+
: undefined;
|
|
376
|
+
const ownerId = capturedOptions.kernelOwnerId ?? sessionId;
|
|
377
|
+
const deadlineMs = getExecutionDeadlineMs(capturedOptions);
|
|
378
|
+
const controller = new AbortController();
|
|
379
|
+
const { promise: completion, resolve } = Promise.withResolvers<void>();
|
|
380
|
+
let deadlineTimer: NodeJS.Timeout | undefined;
|
|
381
|
+
const request: ActivePythonRequest = {
|
|
382
|
+
ownerId,
|
|
383
|
+
sessionId,
|
|
384
|
+
fallbackOwner: capturedOptions.kernelOwnerId === undefined,
|
|
385
|
+
controller,
|
|
386
|
+
completion,
|
|
387
|
+
kernels: new Set(),
|
|
388
|
+
cleanupAttempts: new Map(),
|
|
389
|
+
finish: () => {
|
|
390
|
+
detachInitializerRequest(request, false);
|
|
391
|
+
detachProvisionalSessionRequest(request);
|
|
392
|
+
activeRequests.delete(request);
|
|
393
|
+
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
394
|
+
resolve();
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
activeRequests.add(request);
|
|
398
|
+
|
|
399
|
+
const signals = [controller.signal];
|
|
400
|
+
if (capturedOptions.signal) signals.push(capturedOptions.signal);
|
|
401
|
+
const signal = AbortSignal.any(signals);
|
|
402
|
+
request.signal = signal;
|
|
403
|
+
if (sessionId !== undefined) {
|
|
404
|
+
const session = sessions.get(sessionId);
|
|
405
|
+
if (session && isInitializingSession(session)) enrollInitializerRequest(session, request);
|
|
406
|
+
}
|
|
407
|
+
const remainingMs = getRemainingTimeoutMs(deadlineMs);
|
|
408
|
+
deadlineTimer =
|
|
409
|
+
remainingMs !== undefined
|
|
410
|
+
? setTimeout(() => controller.abort(new PythonExecutionCancelledError(true)), Math.max(0, remainingMs))
|
|
411
|
+
: undefined;
|
|
412
|
+
deadlineTimer?.unref?.();
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
options: { ...capturedOptions, signal, deadlineMs },
|
|
416
|
+
request,
|
|
417
|
+
cwd,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
205
421
|
function getRemainingTimeoutMs(deadlineMs?: number): number | undefined {
|
|
206
422
|
if (deadlineMs === undefined) return undefined;
|
|
207
423
|
return deadlineMs - Date.now();
|
|
@@ -233,6 +449,13 @@ function isTimedOutCancellation(error: unknown, signal?: AbortSignal): boolean {
|
|
|
233
449
|
return reason instanceof Error ? reason.name === "TimeoutError" : false;
|
|
234
450
|
}
|
|
235
451
|
|
|
452
|
+
function throwIfExecutionCancelled(options: Pick<PythonExecutorOptions, "signal" | "deadlineMs">): void {
|
|
453
|
+
if (options.signal?.aborted) {
|
|
454
|
+
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
455
|
+
}
|
|
456
|
+
requireRemainingTimeoutMs(options.deadlineMs);
|
|
457
|
+
}
|
|
458
|
+
|
|
236
459
|
async function waitForPromiseWithCancellation<T>(
|
|
237
460
|
promise: Promise<T>,
|
|
238
461
|
options: Pick<PythonExecutorOptions, "signal" | "deadlineMs">,
|
|
@@ -332,8 +555,15 @@ function buildKernelEnv(options: {
|
|
|
332
555
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
333
556
|
}
|
|
334
557
|
|
|
335
|
-
async function startKernel(
|
|
336
|
-
|
|
558
|
+
async function startKernel(
|
|
559
|
+
cwd: string,
|
|
560
|
+
options: PythonExecutorOptions,
|
|
561
|
+
registration?: {
|
|
562
|
+
onKernelCreated: (kernel: PythonKernel) => void;
|
|
563
|
+
onStartupFailure: (kernel: PythonKernel) => Promise<void>;
|
|
564
|
+
},
|
|
565
|
+
): Promise<PythonKernel> {
|
|
566
|
+
throwIfExecutionCancelled(options);
|
|
337
567
|
return await PythonKernel.start({
|
|
338
568
|
cwd,
|
|
339
569
|
settings: options.settings,
|
|
@@ -341,66 +571,259 @@ async function startKernel(cwd: string, options: PythonExecutorOptions): Promise
|
|
|
341
571
|
runtimeOptions: options.runtimeOptions,
|
|
342
572
|
signal: options.signal,
|
|
343
573
|
deadlineMs: options.deadlineMs,
|
|
574
|
+
onKernelCreated: registration?.onKernelCreated,
|
|
575
|
+
onStartupFailure: registration?.onStartupFailure,
|
|
344
576
|
});
|
|
345
577
|
}
|
|
346
578
|
|
|
347
|
-
function attachOwner(
|
|
579
|
+
function attachOwner(
|
|
580
|
+
session: PythonSession | InitializingPythonSession,
|
|
581
|
+
sessionId: string,
|
|
582
|
+
ownerId: string | undefined,
|
|
583
|
+
): void {
|
|
348
584
|
if (ownerId !== undefined) {
|
|
349
585
|
if (session.hasFallbackOwner) {
|
|
350
586
|
session.ownerIds.delete(sessionId);
|
|
587
|
+
if (!isInitializingSession(session)) session.durableOwnerIds.delete(sessionId);
|
|
588
|
+
removeKernelOwner(session.kernel, sessionId);
|
|
351
589
|
session.hasFallbackOwner = false;
|
|
352
590
|
}
|
|
353
591
|
session.ownerIds.add(ownerId);
|
|
592
|
+
if (!isInitializingSession(session)) session.durableOwnerIds.add(ownerId);
|
|
593
|
+
retainKernelForOwner(session.kernel, ownerId);
|
|
354
594
|
return;
|
|
355
595
|
}
|
|
356
596
|
if (session.hasFallbackOwner || session.ownerIds.size === 0) {
|
|
357
597
|
session.ownerIds.add(sessionId);
|
|
598
|
+
if (!isInitializingSession(session)) session.durableOwnerIds.add(sessionId);
|
|
358
599
|
session.hasFallbackOwner = true;
|
|
600
|
+
retainKernelForOwner(session.kernel, sessionId);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function enrollInitializerRequest(initializing: InitializingPythonSession, request: ActivePythonRequest): boolean {
|
|
605
|
+
if (request.initializer === initializing) return true;
|
|
606
|
+
if (request.signal?.aborted || initializing.cancelled) return false;
|
|
607
|
+
if (request.sessionId !== initializing.sessionId) return false;
|
|
608
|
+
if (request.initializer) detachInitializerRequest(request, false);
|
|
609
|
+
request.initializer = initializing;
|
|
610
|
+
initializing.requests.add(request);
|
|
611
|
+
if (request.fallbackOwner || !initializing.hasFallbackOwner) {
|
|
612
|
+
attachOwner(initializing, initializing.sessionId, request.fallbackOwner ? undefined : request.ownerId);
|
|
613
|
+
} else if (request.ownerId !== undefined) {
|
|
614
|
+
initializing.ownerIds.add(request.ownerId);
|
|
615
|
+
}
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function initializerRequestOwnerId(request: ActivePythonRequest): string | undefined {
|
|
620
|
+
return request.ownerId;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function detachInitializerRequest(request: ActivePythonRequest, timedOut: boolean): void {
|
|
624
|
+
const initializing = request.initializer;
|
|
625
|
+
if (!initializing) return;
|
|
626
|
+
request.initializer = undefined;
|
|
627
|
+
request.initializerAcquired = false;
|
|
628
|
+
initializing.requests.delete(request);
|
|
629
|
+
const ownerId = initializerRequestOwnerId(request);
|
|
630
|
+
if (ownerId !== undefined) {
|
|
631
|
+
let hasOwnerRequest = false;
|
|
632
|
+
for (const other of initializing.requests) {
|
|
633
|
+
if (other.ownerId === ownerId && !other.signal?.aborted) {
|
|
634
|
+
hasOwnerRequest = true;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (!hasOwnerRequest) {
|
|
639
|
+
initializing.ownerIds.delete(ownerId);
|
|
640
|
+
if (request.fallbackOwner || initializing.cleanupFailed) {
|
|
641
|
+
initializing.retirementOwnerIds.add(ownerId);
|
|
642
|
+
}
|
|
643
|
+
if (initializing.ownerIds.size > 0) removeKernelOwner(initializing.kernel, ownerId);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (initializing.ownerIds.size !== 0 || initializing.cancelled) return;
|
|
647
|
+
initializing.cancelled = new PythonExecutionCancelledError(timedOut);
|
|
648
|
+
if (initializing.kernel && !initializing.cleanupAttempt && !initializing.cleanupFailed) {
|
|
649
|
+
const [cleanup, start] = prepareRetiringKernelShutdown(initializing.kernel);
|
|
650
|
+
initializing.cleanupAttempt = cleanup;
|
|
651
|
+
start();
|
|
652
|
+
}
|
|
653
|
+
initializing.startupController.abort(initializing.cancelled);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function promoteInitializerRequest(request: ActivePythonRequest): void {
|
|
657
|
+
const initializing = request.initializer;
|
|
658
|
+
if (!initializing) return;
|
|
659
|
+
initializing.requests.delete(request);
|
|
660
|
+
request.initializer = undefined;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function promoteProvisionalSessionRequest(request: ActivePythonRequest): void {
|
|
664
|
+
const session = request.provisionalSession;
|
|
665
|
+
if (!session) return;
|
|
666
|
+
request.provisionalSession = undefined;
|
|
667
|
+
const ownerId = request.ownerId;
|
|
668
|
+
if (ownerId === undefined) return;
|
|
669
|
+
session.durableOwnerIds.add(ownerId);
|
|
670
|
+
const count = session.provisionalOwnerCounts.get(ownerId) ?? 0;
|
|
671
|
+
if (count <= 1) session.provisionalOwnerCounts.delete(ownerId);
|
|
672
|
+
else session.provisionalOwnerCounts.set(ownerId, count - 1);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function detachProvisionalSessionRequest(request: ActivePythonRequest): void {
|
|
676
|
+
const session = request.provisionalSession;
|
|
677
|
+
if (!session) return;
|
|
678
|
+
request.provisionalSession = undefined;
|
|
679
|
+
const ownerId = request.ownerId;
|
|
680
|
+
if (ownerId === undefined) return;
|
|
681
|
+
const count = session.provisionalOwnerCounts.get(ownerId) ?? 0;
|
|
682
|
+
if (count <= 1) session.provisionalOwnerCounts.delete(ownerId);
|
|
683
|
+
else session.provisionalOwnerCounts.set(ownerId, count - 1);
|
|
684
|
+
if (count > 1 || session.durableOwnerIds.has(ownerId)) return;
|
|
685
|
+
if (session.ownerIds.size === 1 && session.ownerIds.has(ownerId)) {
|
|
686
|
+
retainKernelForOwner(session.kernel, ownerId);
|
|
687
|
+
if (sessions.get(session.sessionId) === session) sessions.delete(session.sessionId);
|
|
688
|
+
const cleanup = prepareSessionShutdown(session);
|
|
689
|
+
startSessionShutdown(session);
|
|
690
|
+
void cleanup.catch(error =>
|
|
691
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: session.kernel.id, reason: error }),
|
|
692
|
+
);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
session.ownerIds.delete(ownerId);
|
|
696
|
+
removeKernelOwner(session.kernel, ownerId);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function initializingOwnerIds(initializing: InitializingPythonSession): Set<string> {
|
|
700
|
+
return new Set(initializing.ownerIds.size > 0 ? initializing.ownerIds : initializing.retirementOwnerIds);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function shutdownInitializingKernel(
|
|
704
|
+
initializing: InitializingPythonSession,
|
|
705
|
+
kernel: PythonKernel,
|
|
706
|
+
): Promise<void> {
|
|
707
|
+
try {
|
|
708
|
+
if (initializing.kernel === kernel && initializing.cleanupAttempt) {
|
|
709
|
+
await initializing.cleanupAttempt;
|
|
710
|
+
} else {
|
|
711
|
+
await shutdownOrRetainKernel(kernel, initializingOwnerIds(initializing));
|
|
712
|
+
}
|
|
713
|
+
} catch (error) {
|
|
714
|
+
initializing.cleanupError = error;
|
|
715
|
+
initializing.cleanupFailed = true;
|
|
716
|
+
throw error;
|
|
359
717
|
}
|
|
360
718
|
}
|
|
361
719
|
|
|
362
|
-
async function acquireSession(
|
|
720
|
+
async function acquireSession(
|
|
721
|
+
sessionId: string,
|
|
722
|
+
cwd: string,
|
|
723
|
+
options: PythonExecutorOptions,
|
|
724
|
+
request: ActivePythonRequest,
|
|
725
|
+
): Promise<PythonSession> {
|
|
726
|
+
throwIfExecutionCancelled(options);
|
|
363
727
|
const existing = sessions.get(sessionId);
|
|
728
|
+
if (request.provisionalSession && request.provisionalSession !== existing) {
|
|
729
|
+
detachProvisionalSessionRequest(request);
|
|
730
|
+
}
|
|
364
731
|
if (existing) {
|
|
732
|
+
if (isInitializingSession(existing)) {
|
|
733
|
+
if (enrollInitializerRequest(existing, request)) {
|
|
734
|
+
if (!request.fallbackOwner) attachOwner(existing, sessionId, options.kernelOwnerId);
|
|
735
|
+
request.initializerAcquired = true;
|
|
736
|
+
}
|
|
737
|
+
} else {
|
|
738
|
+
promoteProvisionalSessionRequest(request);
|
|
739
|
+
attachOwner(existing, sessionId, options.kernelOwnerId);
|
|
740
|
+
}
|
|
741
|
+
if (isInitializingSession(existing) && existing.cleanupFailed && existing.kernel) {
|
|
742
|
+
await shutdownOrRetainKernel(existing.kernel, initializingOwnerIds(existing));
|
|
743
|
+
promoteInitializerRequest(request);
|
|
744
|
+
existing.cleanupFailed = false;
|
|
745
|
+
existing.cleanupError = undefined;
|
|
746
|
+
existing.ownerIds.clear();
|
|
747
|
+
existing.retirementOwnerIds.clear();
|
|
748
|
+
existing.hasFallbackOwner = false;
|
|
749
|
+
if (sessions.get(sessionId) === existing) sessions.delete(sessionId);
|
|
750
|
+
throwIfExecutionCancelled(options);
|
|
751
|
+
return await acquireSession(sessionId, cwd, options, request);
|
|
752
|
+
}
|
|
365
753
|
let session: PythonSession;
|
|
366
754
|
if (isInitializingSession(existing)) {
|
|
367
755
|
try {
|
|
368
756
|
session = await waitForPromiseWithCancellation(existing.promise, options);
|
|
369
757
|
} catch (error) {
|
|
758
|
+
if (isCancellationError(error))
|
|
759
|
+
detachInitializerRequest(request, isTimedOutCancellation(error, options.signal));
|
|
370
760
|
const remainingMs = getRemainingTimeoutMs(options.deadlineMs);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return await acquireSession(sessionId, cwd, options);
|
|
761
|
+
const mayRetry = !options.signal?.aborted && (remainingMs === undefined || remainingMs > 0);
|
|
762
|
+
if (isCancellationError(error) && mayRetry && !existing.cleanupFailed) {
|
|
763
|
+
if (existing.cancelled) await existing.promise.catch(() => undefined);
|
|
764
|
+
return await acquireSession(sessionId, cwd, options, request);
|
|
765
|
+
}
|
|
377
766
|
throw error;
|
|
378
767
|
}
|
|
379
768
|
} else {
|
|
380
769
|
session = existing;
|
|
381
770
|
}
|
|
382
|
-
|
|
383
|
-
ensurePythonResourceCleanup();
|
|
771
|
+
if (isInitializingSession(existing)) promoteInitializerRequest(request);
|
|
384
772
|
return session;
|
|
385
773
|
}
|
|
386
774
|
|
|
775
|
+
const startupController = new AbortController();
|
|
387
776
|
const initializing: InitializingPythonSession = {
|
|
388
777
|
sessionId,
|
|
778
|
+
startupController,
|
|
779
|
+
requests: new Set(),
|
|
780
|
+
ownerIds: new Set(),
|
|
781
|
+
retirementOwnerIds: new Set(),
|
|
782
|
+
hasFallbackOwner: false,
|
|
389
783
|
promise: Promise.resolve().then(async () => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
784
|
+
if (initializing.cancelled) throw initializing.cancelled;
|
|
785
|
+
const startupOptions = { ...options, signal: startupController.signal, deadlineMs: undefined };
|
|
786
|
+
const kernel = await startKernel(cwd, startupOptions, {
|
|
787
|
+
onKernelCreated: created => {
|
|
788
|
+
initializing.kernel = created;
|
|
789
|
+
retainKernelForOwners(created, initializingOwnerIds(initializing));
|
|
790
|
+
},
|
|
791
|
+
onStartupFailure: async failed => {
|
|
792
|
+
try {
|
|
793
|
+
await shutdownInitializingKernel(initializing, failed);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
logger.warn("Python kernel shutdown not confirmed", {
|
|
796
|
+
kernelId: failed.id,
|
|
797
|
+
reason: error,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
for (const route of [...initializing.requests]) {
|
|
803
|
+
if (!route.initializerAcquired && route.signal?.aborted) {
|
|
804
|
+
detachInitializerRequest(route, isTimedOutCancellation(route.signal.reason, route.signal));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (initializing.cancelled || startupController.signal.aborted) {
|
|
808
|
+
try {
|
|
809
|
+
await shutdownInitializingKernel(initializing, kernel);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id, reason: error });
|
|
812
|
+
}
|
|
813
|
+
throw (
|
|
814
|
+
initializing.cancelled ??
|
|
815
|
+
new PythonExecutionCancelledError(
|
|
816
|
+
isTimedOutCancellation(startupController.signal.reason, startupController.signal),
|
|
817
|
+
)
|
|
818
|
+
);
|
|
394
819
|
}
|
|
395
820
|
const current = sessions.get(sessionId);
|
|
396
821
|
if (current !== initializing) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
: undefined;
|
|
403
|
-
if (winner) return winner;
|
|
822
|
+
try {
|
|
823
|
+
await shutdownInitializingKernel(initializing, kernel);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id, reason: error });
|
|
826
|
+
}
|
|
404
827
|
throw new PythonExecutionCancelledError(false);
|
|
405
828
|
}
|
|
406
829
|
const session: PythonSession = {
|
|
@@ -408,21 +831,36 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
408
831
|
kernel,
|
|
409
832
|
kernelInstanceId: crypto.randomUUID(),
|
|
410
833
|
bridgeCapability: options.bridge?.capability,
|
|
411
|
-
ownerIds: new Set(),
|
|
412
|
-
|
|
834
|
+
ownerIds: new Set(initializing.ownerIds),
|
|
835
|
+
durableOwnerIds: new Set(),
|
|
836
|
+
provisionalOwnerCounts: new Map(),
|
|
837
|
+
hasFallbackOwner: initializing.hasFallbackOwner,
|
|
413
838
|
queue: Promise.resolve(),
|
|
414
839
|
};
|
|
840
|
+
for (const route of initializing.requests) {
|
|
841
|
+
const ownerId = route.ownerId;
|
|
842
|
+
const acquired = route.initializerAcquired === true;
|
|
843
|
+
route.initializer = undefined;
|
|
844
|
+
route.initializerAcquired = false;
|
|
845
|
+
if (ownerId === undefined || !session.ownerIds.has(ownerId)) continue;
|
|
846
|
+
if (acquired) {
|
|
847
|
+
session.durableOwnerIds.add(ownerId);
|
|
848
|
+
} else {
|
|
849
|
+
route.provisionalSession = session;
|
|
850
|
+
session.provisionalOwnerCounts.set(ownerId, (session.provisionalOwnerCounts.get(ownerId) ?? 0) + 1);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
initializing.requests.clear();
|
|
415
854
|
sessions.set(sessionId, session);
|
|
416
855
|
return session;
|
|
417
856
|
}),
|
|
418
857
|
};
|
|
858
|
+
if (enrollInitializerRequest(initializing, request)) request.initializerAcquired = true;
|
|
859
|
+
initializingSessions.add(initializing);
|
|
419
860
|
sessions.set(sessionId, initializing);
|
|
420
|
-
ensurePythonResourceCleanup();
|
|
421
861
|
let cancellationTimer: NodeJS.Timeout | undefined;
|
|
422
862
|
const retireCancelledInitialization = (timedOut: boolean): void => {
|
|
423
|
-
|
|
424
|
-
initializing.cancelled = new PythonExecutionCancelledError(timedOut);
|
|
425
|
-
if (sessions.get(sessionId) === initializing) sessions.delete(sessionId);
|
|
863
|
+
detachInitializerRequest(request, timedOut);
|
|
426
864
|
};
|
|
427
865
|
const onAbort = (): void => {
|
|
428
866
|
options.signal?.removeEventListener("abort", onAbort);
|
|
@@ -444,20 +882,21 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
444
882
|
}
|
|
445
883
|
void initializing.promise
|
|
446
884
|
.finally(() => {
|
|
885
|
+
initializingSessions.delete(initializing);
|
|
886
|
+
if (!initializing.cleanupFailed && sessions.get(sessionId) === initializing) sessions.delete(sessionId);
|
|
447
887
|
options.signal?.removeEventListener("abort", onAbort);
|
|
448
888
|
if (cancellationTimer) clearTimeout(cancellationTimer);
|
|
449
889
|
})
|
|
450
890
|
.catch(() => undefined);
|
|
451
891
|
try {
|
|
452
892
|
const session = await waitForPromiseWithCancellation(initializing.promise, options);
|
|
453
|
-
|
|
454
|
-
ensurePythonResourceCleanup();
|
|
893
|
+
promoteInitializerRequest(request);
|
|
455
894
|
return session;
|
|
456
895
|
} catch (err) {
|
|
457
896
|
if (isCancellationError(err)) {
|
|
458
897
|
retireCancelledInitialization(isTimedOutCancellation(err, options.signal));
|
|
459
|
-
await initializing.promise.catch(() => undefined);
|
|
460
|
-
} else if (sessions.get(sessionId) === initializing) {
|
|
898
|
+
if (initializing.cancelled) await initializing.promise.catch(() => undefined);
|
|
899
|
+
} else if (sessions.get(sessionId) === initializing && !initializing.cleanupFailed) {
|
|
461
900
|
sessions.delete(sessionId);
|
|
462
901
|
}
|
|
463
902
|
throw err;
|
|
@@ -469,22 +908,33 @@ async function replaceSessionKernel(
|
|
|
469
908
|
cwd: string,
|
|
470
909
|
options: PythonExecutorOptions,
|
|
471
910
|
): Promise<void> {
|
|
911
|
+
throwIfExecutionCancelled(options);
|
|
472
912
|
const old = session.kernel;
|
|
473
913
|
const remaining = getRemainingTimeoutMs(options.deadlineMs);
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
914
|
+
const ownerIds = new Set(session.ownerIds);
|
|
915
|
+
await shutdownOrRetainKernel(old, ownerIds, remaining === undefined ? undefined : Math.max(0, remaining));
|
|
916
|
+
throwIfExecutionCancelled(options);
|
|
477
917
|
if (sessions.get(session.sessionId) !== session) {
|
|
478
918
|
throw new PythonExecutionCancelledError(false);
|
|
479
919
|
}
|
|
480
|
-
|
|
920
|
+
throwIfExecutionCancelled(options);
|
|
481
921
|
const bridge = options.bridge;
|
|
482
922
|
const previousCapability = bridge?.capability;
|
|
483
923
|
const nextCapability = bridge ? crypto.randomUUID() : undefined;
|
|
484
924
|
if (bridge && nextCapability) bridge.capability = nextCapability;
|
|
485
925
|
let next: PythonKernel | undefined;
|
|
926
|
+
let startSucceeded = false;
|
|
486
927
|
try {
|
|
487
|
-
|
|
928
|
+
throwIfExecutionCancelled(options);
|
|
929
|
+
next = await startKernel(cwd, options, {
|
|
930
|
+
onKernelCreated: created => {
|
|
931
|
+
next = created;
|
|
932
|
+
retainKernelForOwners(created, ownerIds);
|
|
933
|
+
},
|
|
934
|
+
onStartupFailure: failed => shutdownOrRetainKernel(failed, ownerIds),
|
|
935
|
+
});
|
|
936
|
+
startSucceeded = true;
|
|
937
|
+
throwIfExecutionCancelled(options);
|
|
488
938
|
if (sessions.get(session.sessionId) !== session) {
|
|
489
939
|
throw new PythonExecutionCancelledError(false);
|
|
490
940
|
}
|
|
@@ -492,7 +942,7 @@ async function replaceSessionKernel(
|
|
|
492
942
|
session.kernelInstanceId = crypto.randomUUID();
|
|
493
943
|
session.bridgeCapability = nextCapability;
|
|
494
944
|
} catch (err) {
|
|
495
|
-
await next
|
|
945
|
+
if (startSucceeded && next) await shutdownOrRetainKernel(next, ownerIds).catch(() => undefined);
|
|
496
946
|
if (bridge && previousCapability && bridge.capability === nextCapability) {
|
|
497
947
|
bridge.capability = previousCapability;
|
|
498
948
|
}
|
|
@@ -503,9 +953,26 @@ async function replaceSessionKernel(
|
|
|
503
953
|
async function resetSession(sessionId: string): Promise<void> {
|
|
504
954
|
const existing = sessions.get(sessionId);
|
|
505
955
|
if (!existing) return;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
956
|
+
if (isInitializingSession(existing)) {
|
|
957
|
+
existing.cancelled ??= new PythonExecutionCancelledError(false);
|
|
958
|
+
let cleanup: Promise<KernelShutdownResult> | undefined;
|
|
959
|
+
if (existing.kernel) {
|
|
960
|
+
const prepared = prepareRetiringKernelShutdown(existing.kernel);
|
|
961
|
+
cleanup = prepared[0];
|
|
962
|
+
existing.cleanupAttempt = cleanup;
|
|
963
|
+
prepared[1]();
|
|
964
|
+
}
|
|
965
|
+
if (!existing.startupController.signal.aborted) existing.startupController.abort(existing.cancelled);
|
|
966
|
+
await existing.promise.catch(() => undefined);
|
|
967
|
+
if (existing.cleanupFailed) throw existing.cleanupError;
|
|
968
|
+
if (cleanup) await cleanup;
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const kernel = existing.kernel;
|
|
972
|
+
const shutdown = prepareSessionShutdown(existing);
|
|
973
|
+
startSessionShutdown(existing);
|
|
974
|
+
await shutdown;
|
|
975
|
+
if (sessions.get(sessionId) === existing && existing.kernel === kernel) sessions.delete(sessionId);
|
|
509
976
|
}
|
|
510
977
|
|
|
511
978
|
async function runQueued<T>(
|
|
@@ -533,68 +1000,192 @@ async function runQueued<T>(
|
|
|
533
1000
|
// Public dispose entry points
|
|
534
1001
|
// ---------------------------------------------------------------------------
|
|
535
1002
|
|
|
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,
|
|
1003
|
+
export function disposeAllKernelSessions(): Promise<void> {
|
|
1004
|
+
const entries = [...sessions.entries()];
|
|
1005
|
+
const requests = [...activeRequests];
|
|
1006
|
+
const initializing = [...initializingSessions];
|
|
1007
|
+
const targets = new Set<PythonSession>([
|
|
1008
|
+
...[...entries.map(([, entry]) => entry), ...retiringSessions].filter(
|
|
1009
|
+
(entry): entry is PythonSession => !isInitializingSession(entry),
|
|
547
1010
|
),
|
|
548
|
-
);
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
});
|
|
1011
|
+
]);
|
|
1012
|
+
const shutdowns = [...targets].map(session => [session, prepareSessionShutdown(session)] as const);
|
|
1013
|
+
const retiring = [...retiringKernels].map(kernel => [kernel, ...prepareRetiringKernelShutdown(kernel)] as const);
|
|
1014
|
+
for (const request of requests) {
|
|
1015
|
+
for (const kernel of request.kernels) {
|
|
1016
|
+
const cleanup = retiring.find(([retiringKernel]) => retiringKernel === kernel)?.[1];
|
|
1017
|
+
if (cleanup) request.cleanupAttempts.set(kernel, cleanup);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
for (const entry of initializing) {
|
|
1021
|
+
if (!entry.kernel) continue;
|
|
1022
|
+
const cleanup = retiring.find(([kernel]) => kernel === entry.kernel)?.[1];
|
|
1023
|
+
if (cleanup) entry.cleanupAttempt = cleanup;
|
|
570
1024
|
}
|
|
1025
|
+
for (const [id, entry] of entries) {
|
|
1026
|
+
if (sessions.get(id) === entry) sessions.delete(id);
|
|
1027
|
+
}
|
|
1028
|
+
// Publish every exact physical cleanup future before abort listeners or shutdown callbacks can reenter disposal.
|
|
1029
|
+
for (const request of requests) {
|
|
1030
|
+
if (!request.controller.signal.aborted) request.controller.abort(new PythonExecutionCancelledError(false));
|
|
1031
|
+
}
|
|
1032
|
+
for (const entry of initializing) {
|
|
1033
|
+
entry.cancelled ??= new PythonExecutionCancelledError(false);
|
|
1034
|
+
if (!entry.startupController.signal.aborted) entry.startupController.abort(entry.cancelled);
|
|
1035
|
+
}
|
|
1036
|
+
for (const [session] of shutdowns) startSessionShutdown(session);
|
|
1037
|
+
for (const [, , start] of retiring) start();
|
|
1038
|
+
return (async () => {
|
|
1039
|
+
await Promise.all([
|
|
1040
|
+
...requests.map(request => request.completion),
|
|
1041
|
+
...initializing.map(entry => entry.promise.catch(() => undefined)),
|
|
1042
|
+
]);
|
|
1043
|
+
const failures: unknown[] = initializing.filter(entry => entry.cleanupFailed).map(entry => entry.cleanupError);
|
|
1044
|
+
const results = await Promise.allSettled(shutdowns.map(([, cleanup]) => cleanup));
|
|
1045
|
+
for (let index = 0; index < shutdowns.length; index += 1) {
|
|
1046
|
+
const [session] = shutdowns[index];
|
|
1047
|
+
const result = results[index];
|
|
1048
|
+
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
1049
|
+
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, session);
|
|
1050
|
+
if (result.status === "rejected") failures.push(result.reason);
|
|
1051
|
+
else failures.push(unconfirmedShutdownError(session.kernel));
|
|
1052
|
+
}
|
|
1053
|
+
const retiringResults = await Promise.allSettled(retiring.map(([, cleanup]) => cleanup));
|
|
1054
|
+
for (let index = 0; index < retiring.length; index += 1) {
|
|
1055
|
+
const [kernel] = retiring[index];
|
|
1056
|
+
const result = retiringResults[index];
|
|
1057
|
+
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
1058
|
+
retiringKernels.add(kernel);
|
|
1059
|
+
failures.push(result.status === "rejected" ? result.reason : unconfirmedShutdownError(kernel));
|
|
1060
|
+
}
|
|
1061
|
+
if (failures.length) throw failures[0];
|
|
1062
|
+
})();
|
|
571
1063
|
}
|
|
572
1064
|
|
|
573
|
-
export
|
|
1065
|
+
export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
1066
|
+
const requests = [...activeRequests].filter(request => request.ownerId === ownerId);
|
|
1067
|
+
const entries = [...sessions.entries()].filter(([, entry]) => entry.ownerIds.has(ownerId));
|
|
1068
|
+
const initializing = [...initializingSessions].filter(
|
|
1069
|
+
entry => entry.ownerIds.has(ownerId) || entry.retirementOwnerIds.has(ownerId),
|
|
1070
|
+
);
|
|
1071
|
+
const sessionsToRetire = new Set<PythonSession>([
|
|
1072
|
+
...[...entries.map(([, entry]) => entry), ...retiringSessions].filter(
|
|
1073
|
+
(entry): entry is PythonSession => !isInitializingSession(entry) && entry.ownerIds.has(ownerId),
|
|
1074
|
+
),
|
|
1075
|
+
]);
|
|
574
1076
|
const toShutdown: PythonSession[] = [];
|
|
575
|
-
|
|
576
|
-
|
|
1077
|
+
const initializersToAbort: InitializingPythonSession[] = [];
|
|
1078
|
+
for (const entry of initializing) {
|
|
1079
|
+
entry.retirementOwnerIds.add(ownerId);
|
|
1080
|
+
let hasOwnerRequest = false;
|
|
1081
|
+
for (const request of entry.requests) {
|
|
1082
|
+
if (request.ownerId === ownerId && !request.signal?.aborted) {
|
|
1083
|
+
hasOwnerRequest = true;
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (!hasOwnerRequest) {
|
|
1088
|
+
entry.ownerIds.delete(ownerId);
|
|
1089
|
+
if (entry.ownerIds.size > 0) removeKernelOwner(entry.kernel, ownerId);
|
|
1090
|
+
}
|
|
1091
|
+
if (![...entry.ownerIds].some(id => id !== ownerId)) {
|
|
1092
|
+
entry.cancelled ??= new PythonExecutionCancelledError(false);
|
|
1093
|
+
initializersToAbort.push(entry);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
for (const session of sessionsToRetire) {
|
|
577
1097
|
if (session.ownerIds.size === 1) {
|
|
578
1098
|
toShutdown.push(session);
|
|
579
|
-
|
|
1099
|
+
if (sessions.get(session.sessionId) === session) sessions.delete(session.sessionId);
|
|
1100
|
+
} else {
|
|
1101
|
+
session.ownerIds.delete(ownerId);
|
|
1102
|
+
session.durableOwnerIds.delete(ownerId);
|
|
1103
|
+
removeKernelOwner(session.kernel, ownerId);
|
|
580
1104
|
}
|
|
581
|
-
session.ownerIds.delete(ownerId);
|
|
582
1105
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (result.status === "fulfilled" && result.value.confirmed) {
|
|
591
|
-
session.ownerIds.delete(ownerId);
|
|
1106
|
+
const shutdowns = toShutdown.map(session => [session, prepareSessionShutdown(session)] as const);
|
|
1107
|
+
const retiring: Array<readonly [PythonKernel, Promise<KernelShutdownResult>, () => void]> = [];
|
|
1108
|
+
for (const kernel of retiringKernels) {
|
|
1109
|
+
const owners = retiringKernelOwners.get(kernel);
|
|
1110
|
+
if (!owners?.has(ownerId)) continue;
|
|
1111
|
+
if (owners.size > 1) {
|
|
1112
|
+
owners.delete(ownerId);
|
|
592
1113
|
continue;
|
|
593
1114
|
}
|
|
594
|
-
|
|
595
|
-
logger.warn("Python kernel shutdown not confirmed", { sessionId: session.sessionId, reason });
|
|
596
|
-
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, session);
|
|
1115
|
+
retiring.push([kernel, ...prepareRetiringKernelShutdown(kernel)]);
|
|
597
1116
|
}
|
|
1117
|
+
for (const request of requests) {
|
|
1118
|
+
for (const kernel of request.kernels) {
|
|
1119
|
+
const cleanup = retiring.find(([retiringKernel]) => retiringKernel === kernel)?.[1];
|
|
1120
|
+
if (cleanup) request.cleanupAttempts.set(kernel, cleanup);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
for (const entry of initializing) {
|
|
1124
|
+
if (!entry.kernel) continue;
|
|
1125
|
+
const cleanup = retiring.find(([kernel]) => kernel === entry.kernel)?.[1];
|
|
1126
|
+
if (cleanup) entry.cleanupAttempt = cleanup;
|
|
1127
|
+
}
|
|
1128
|
+
// All cleanup futures are visible before an abort listener can reenter this API.
|
|
1129
|
+
for (const entry of initializersToAbort) {
|
|
1130
|
+
if (!entry.startupController.signal.aborted) entry.startupController.abort(entry.cancelled);
|
|
1131
|
+
}
|
|
1132
|
+
for (const request of requests) {
|
|
1133
|
+
if (!request.controller.signal.aborted) request.controller.abort(new PythonExecutionCancelledError(false));
|
|
1134
|
+
}
|
|
1135
|
+
for (const [session] of shutdowns) startSessionShutdown(session);
|
|
1136
|
+
for (const [, , start] of retiring) start();
|
|
1137
|
+
return (async () => {
|
|
1138
|
+
await Promise.all([
|
|
1139
|
+
...requests.map(request => request.completion),
|
|
1140
|
+
...initializing.map(entry => entry.promise.catch(() => undefined)),
|
|
1141
|
+
]);
|
|
1142
|
+
const failures: unknown[] = [];
|
|
1143
|
+
for (const entry of initializing)
|
|
1144
|
+
if (entry.cleanupFailed) {
|
|
1145
|
+
if (entry.kernel) retainKernelForOwners(entry.kernel, new Set([ownerId]));
|
|
1146
|
+
failures.push(entry.cleanupError);
|
|
1147
|
+
}
|
|
1148
|
+
const results = await Promise.allSettled(shutdowns.map(([, shutdown]) => shutdown));
|
|
1149
|
+
for (let index = 0; index < shutdowns.length; index += 1) {
|
|
1150
|
+
const [session] = shutdowns[index];
|
|
1151
|
+
const result = results[index];
|
|
1152
|
+
if (result.status === "fulfilled" && result.value.confirmed) {
|
|
1153
|
+
session.ownerIds.delete(ownerId);
|
|
1154
|
+
session.durableOwnerIds.delete(ownerId);
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, session);
|
|
1158
|
+
if (result.status === "rejected") failures.push(result.reason);
|
|
1159
|
+
else failures.push(unconfirmedShutdownError(session.kernel));
|
|
1160
|
+
}
|
|
1161
|
+
const retiringResults = await Promise.allSettled(retiring.map(([, cleanup]) => cleanup));
|
|
1162
|
+
for (let index = 0; index < retiring.length; index += 1) {
|
|
1163
|
+
const [kernel] = retiring[index];
|
|
1164
|
+
const result = retiringResults[index];
|
|
1165
|
+
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
1166
|
+
failures.push(result.status === "rejected" ? result.reason : unconfirmedShutdownError(kernel));
|
|
1167
|
+
}
|
|
1168
|
+
for (const [id, entry] of sessions) {
|
|
1169
|
+
if (
|
|
1170
|
+
isInitializingSession(entry) &&
|
|
1171
|
+
entry.retirementOwnerIds.has(ownerId) &&
|
|
1172
|
+
entry.cleanupFailed &&
|
|
1173
|
+
entry.kernel &&
|
|
1174
|
+
!retiringKernels.has(entry.kernel)
|
|
1175
|
+
) {
|
|
1176
|
+
entry.cleanupFailed = false;
|
|
1177
|
+
entry.cleanupError = undefined;
|
|
1178
|
+
if (sessions.get(id) === entry) sessions.delete(id);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (failures.length) throw failures[0];
|
|
1182
|
+
})();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function unconfirmedShutdownError(kernel: PythonKernel): Error {
|
|
1186
|
+
const error = new Error(`Python kernel shutdown not confirmed: ${kernel.id}`);
|
|
1187
|
+
error.name = "PythonKernelShutdownUnconfirmedError";
|
|
1188
|
+
return error;
|
|
598
1189
|
}
|
|
599
1190
|
|
|
600
1191
|
// ---------------------------------------------------------------------------
|
|
@@ -607,6 +1198,7 @@ async function executeWithKernel(
|
|
|
607
1198
|
options: PythonExecutorOptions | undefined,
|
|
608
1199
|
): Promise<PythonResult> {
|
|
609
1200
|
const settings = options?.settings ?? (await Settings.init());
|
|
1201
|
+
if (options) throwIfExecutionCancelled(options);
|
|
610
1202
|
const sink = new OutputSink({
|
|
611
1203
|
onChunk: options?.onChunk,
|
|
612
1204
|
artifactPath: options?.artifactPath,
|
|
@@ -623,16 +1215,19 @@ async function executeWithKernel(
|
|
|
623
1215
|
((event: JsStatusEvent) => {
|
|
624
1216
|
displayOutputs.push({ type: "status", event });
|
|
625
1217
|
});
|
|
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;
|
|
1218
|
+
let unregisterBridge: (() => void) | null = null;
|
|
634
1219
|
|
|
635
1220
|
try {
|
|
1221
|
+
if (options) throwIfExecutionCancelled(options);
|
|
1222
|
+
unregisterBridge =
|
|
1223
|
+
options?.toolSession && options?.bridgeSessionId && options.bridge
|
|
1224
|
+
? registerPyToolBridge(options.bridgeSessionId, options.bridge.capability, {
|
|
1225
|
+
toolSession: options.toolSession,
|
|
1226
|
+
signal: options.signal,
|
|
1227
|
+
emitStatus,
|
|
1228
|
+
})
|
|
1229
|
+
: null;
|
|
1230
|
+
if (options) throwIfExecutionCancelled(options);
|
|
636
1231
|
executionTimeoutMs = requireRemainingTimeoutMs(deadlineMs);
|
|
637
1232
|
const result = await kernel.execute(code, {
|
|
638
1233
|
signal: options?.signal,
|
|
@@ -640,6 +1235,7 @@ async function executeWithKernel(
|
|
|
640
1235
|
onChunk: text => sink.push(text),
|
|
641
1236
|
onDisplay: output => void displayOutputs.push(output),
|
|
642
1237
|
});
|
|
1238
|
+
if (options) throwIfExecutionCancelled(options);
|
|
643
1239
|
|
|
644
1240
|
if (result.cancelled) {
|
|
645
1241
|
// Prefer the caller-configured timeout for the user-facing annotation.
|
|
@@ -693,7 +1289,7 @@ async function executeWithKernel(
|
|
|
693
1289
|
...(await sink.dump()),
|
|
694
1290
|
};
|
|
695
1291
|
} catch (err) {
|
|
696
|
-
if (isCancellationError(err)
|
|
1292
|
+
if (isCancellationError(err)) {
|
|
697
1293
|
const timedOut = isTimedOutCancellation(err, options?.signal);
|
|
698
1294
|
return {
|
|
699
1295
|
exitCode: undefined,
|
|
@@ -714,6 +1310,7 @@ async function executeWithKernel(
|
|
|
714
1310
|
}
|
|
715
1311
|
|
|
716
1312
|
async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions): Promise<void> {
|
|
1313
|
+
throwIfExecutionCancelled(options);
|
|
717
1314
|
const availability = await checkPythonKernelAvailability(
|
|
718
1315
|
cwd,
|
|
719
1316
|
options.runtimeOptions,
|
|
@@ -723,6 +1320,7 @@ async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions
|
|
|
723
1320
|
},
|
|
724
1321
|
options.settings,
|
|
725
1322
|
);
|
|
1323
|
+
throwIfExecutionCancelled(options);
|
|
726
1324
|
if (!availability.ok) {
|
|
727
1325
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
728
1326
|
}
|
|
@@ -730,29 +1328,81 @@ async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions
|
|
|
730
1328
|
|
|
731
1329
|
async function ensureToolBridge(options: PythonExecutorOptions): Promise<void> {
|
|
732
1330
|
if (!options.toolSession || options.bridge) return;
|
|
1331
|
+
throwIfExecutionCancelled(options);
|
|
733
1332
|
try {
|
|
734
1333
|
const bridge = await ensurePyToolBridge();
|
|
1334
|
+
throwIfExecutionCancelled(options);
|
|
735
1335
|
options.bridge = { ...bridge, capability: crypto.randomUUID() };
|
|
736
1336
|
} catch (err) {
|
|
1337
|
+
if (isCancellationError(err) || options.signal?.aborted) throw err;
|
|
737
1338
|
logger.warn("Failed to start Python tool bridge", {
|
|
738
1339
|
error: err instanceof Error ? err.message : String(err),
|
|
739
1340
|
});
|
|
740
1341
|
}
|
|
741
1342
|
}
|
|
742
1343
|
|
|
743
|
-
async function executePerCall(
|
|
1344
|
+
async function executePerCall(
|
|
1345
|
+
code: string,
|
|
1346
|
+
cwd: string,
|
|
1347
|
+
options: PythonExecutorOptions,
|
|
1348
|
+
request: ActivePythonRequest,
|
|
1349
|
+
): Promise<PythonResult> {
|
|
1350
|
+
throwIfExecutionCancelled(options);
|
|
744
1351
|
if (options.bridge && !options.bridgeSessionId) {
|
|
745
1352
|
options.bridgeSessionId = `py-bridge:${crypto.randomUUID()}`;
|
|
746
1353
|
}
|
|
747
|
-
const
|
|
1354
|
+
const ownerIds = new Set(options.kernelOwnerId === undefined ? [] : [options.kernelOwnerId]);
|
|
1355
|
+
let physicalKernel: PythonKernel | undefined;
|
|
1356
|
+
const kernel = await startKernel(cwd, options, {
|
|
1357
|
+
onKernelCreated: created => {
|
|
1358
|
+
physicalKernel = created;
|
|
1359
|
+
request.kernels.add(created);
|
|
1360
|
+
retainKernelForOwners(created, ownerIds);
|
|
1361
|
+
},
|
|
1362
|
+
onStartupFailure: async failed => {
|
|
1363
|
+
const cleanup = request.cleanupAttempts.get(failed);
|
|
1364
|
+
if (cleanup) await cleanup;
|
|
1365
|
+
else await shutdownOrRetainKernel(failed, ownerIds);
|
|
1366
|
+
},
|
|
1367
|
+
});
|
|
1368
|
+
const createdKernel = physicalKernel;
|
|
1369
|
+
if (!createdKernel || createdKernel !== kernel) {
|
|
1370
|
+
await shutdownOrRetainKernel(kernel, ownerIds);
|
|
1371
|
+
throw new Error("Python kernel creation was not registered");
|
|
1372
|
+
}
|
|
1373
|
+
let execution:
|
|
1374
|
+
| { readonly succeeded: true; readonly result: PythonResult }
|
|
1375
|
+
| { readonly succeeded: false; readonly error: unknown };
|
|
748
1376
|
try {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1377
|
+
throwIfExecutionCancelled(options);
|
|
1378
|
+
execution = { succeeded: true, result: await executeWithKernel(kernel, code, options) };
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
execution = { succeeded: false, error };
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
let cleanup: { readonly succeeded: true } | { readonly succeeded: false; readonly error: unknown };
|
|
1384
|
+
try {
|
|
1385
|
+
const cleanupAttempt = request.cleanupAttempts.get(createdKernel);
|
|
1386
|
+
if (cleanupAttempt) await cleanupAttempt;
|
|
1387
|
+
else await shutdownOrRetainKernel(createdKernel, ownerIds);
|
|
1388
|
+
cleanup = { succeeded: true };
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
cleanup = { succeeded: false, error: new PythonKernelCleanupError(createdKernel, error) };
|
|
752
1391
|
}
|
|
1392
|
+
|
|
1393
|
+
if (!execution.succeeded && !isCancellationError(execution.error)) throw execution.error;
|
|
1394
|
+
if (!cleanup.succeeded) throw cleanup.error;
|
|
1395
|
+
if (!execution.succeeded) throw execution.error;
|
|
1396
|
+
return execution.result;
|
|
753
1397
|
}
|
|
754
1398
|
|
|
755
|
-
async function executeOnSession(
|
|
1399
|
+
async function executeOnSession(
|
|
1400
|
+
code: string,
|
|
1401
|
+
cwd: string,
|
|
1402
|
+
options: PythonExecutorOptions,
|
|
1403
|
+
request: ActivePythonRequest,
|
|
1404
|
+
): Promise<PythonResult> {
|
|
1405
|
+
throwIfExecutionCancelled(options);
|
|
756
1406
|
const sessionId = scopedSessionId(
|
|
757
1407
|
options.sessionId ?? `session:${cwd}`,
|
|
758
1408
|
options.settings,
|
|
@@ -762,28 +1412,32 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut
|
|
|
762
1412
|
options.bridgeSessionId = sessionId;
|
|
763
1413
|
}
|
|
764
1414
|
if (options.reset) {
|
|
1415
|
+
throwIfExecutionCancelled(options);
|
|
765
1416
|
await resetSession(sessionId);
|
|
1417
|
+
throwIfExecutionCancelled(options);
|
|
766
1418
|
}
|
|
767
|
-
const session = await acquireSession(sessionId, cwd, options);
|
|
1419
|
+
const session = await acquireSession(sessionId, cwd, options, request);
|
|
1420
|
+
throwIfExecutionCancelled(options);
|
|
768
1421
|
if (options.bridge && session.bridgeCapability) {
|
|
769
1422
|
options.bridge.capability = session.bridgeCapability;
|
|
770
1423
|
}
|
|
771
1424
|
options.onKernelStart?.(session.kernelInstanceId);
|
|
772
1425
|
return await runQueued(session, options, async () => {
|
|
773
|
-
|
|
774
|
-
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
775
|
-
}
|
|
1426
|
+
throwIfExecutionCancelled(options);
|
|
776
1427
|
if (sessions.get(session.sessionId) !== session) {
|
|
777
1428
|
throw new PythonExecutionCancelledError(false);
|
|
778
1429
|
}
|
|
779
1430
|
if (!session.kernel.isAlive()) {
|
|
1431
|
+
throwIfExecutionCancelled(options);
|
|
780
1432
|
await replaceSessionKernel(session, cwd, options);
|
|
1433
|
+
throwIfExecutionCancelled(options);
|
|
781
1434
|
if (sessions.get(session.sessionId) !== session) {
|
|
782
1435
|
throw new PythonExecutionCancelledError(false);
|
|
783
1436
|
}
|
|
784
1437
|
options.onKernelStart?.(session.kernelInstanceId);
|
|
785
1438
|
}
|
|
786
1439
|
try {
|
|
1440
|
+
throwIfExecutionCancelled(options);
|
|
787
1441
|
return await executeWithKernel(session.kernel, code, options);
|
|
788
1442
|
} catch (err) {
|
|
789
1443
|
if (isCancellationError(err) || options.signal?.aborted) throw err;
|
|
@@ -792,7 +1446,9 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut
|
|
|
792
1446
|
throw new PythonExecutionCancelledError(false);
|
|
793
1447
|
}
|
|
794
1448
|
// Kernel died during execute. Replace it and retry once on a fresh one.
|
|
1449
|
+
throwIfExecutionCancelled(options);
|
|
795
1450
|
await replaceSessionKernel(session, cwd, options);
|
|
1451
|
+
throwIfExecutionCancelled(options);
|
|
796
1452
|
if (sessions.get(session.sessionId) !== session) {
|
|
797
1453
|
throw new PythonExecutionCancelledError(false);
|
|
798
1454
|
}
|
|
@@ -807,59 +1463,45 @@ export async function executePythonWithKernel(
|
|
|
807
1463
|
code: string,
|
|
808
1464
|
options?: PythonExecutorOptions,
|
|
809
1465
|
): Promise<PythonResult> {
|
|
810
|
-
|
|
1466
|
+
const tracked = beginPythonRequest(options);
|
|
1467
|
+
try {
|
|
1468
|
+
return await executeWithKernel(kernel, code, tracked.options);
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
if (isCancellationError(err)) {
|
|
1471
|
+
return createCancelledPythonResult(
|
|
1472
|
+
isTimedOutCancellation(err, tracked.options.signal),
|
|
1473
|
+
tracked.options.timeoutMs,
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
throw err;
|
|
1477
|
+
} finally {
|
|
1478
|
+
tracked.request.finish();
|
|
1479
|
+
}
|
|
811
1480
|
}
|
|
812
1481
|
|
|
813
1482
|
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
|
-
};
|
|
1483
|
+
const tracked = beginPythonRequest(options, true);
|
|
1484
|
+
const executionOptions = tracked.options;
|
|
1485
|
+
const cwd = tracked.cwd!;
|
|
838
1486
|
try {
|
|
839
|
-
|
|
840
|
-
if (executionOptions.signal?.aborted) {
|
|
841
|
-
throw new PythonExecutionCancelledError(
|
|
842
|
-
isTimedOutCancellation(executionOptions.signal.reason, executionOptions.signal),
|
|
843
|
-
);
|
|
844
|
-
}
|
|
1487
|
+
throwIfExecutionCancelled(executionOptions);
|
|
845
1488
|
await ensureKernelAvailable(cwd, executionOptions);
|
|
1489
|
+
throwIfExecutionCancelled(executionOptions);
|
|
846
1490
|
await ensureToolBridge(executionOptions);
|
|
1491
|
+
throwIfExecutionCancelled(executionOptions);
|
|
847
1492
|
|
|
848
1493
|
const kernelMode = executionOptions.kernelMode ?? "session";
|
|
849
1494
|
if (kernelMode === "per-call") {
|
|
850
|
-
return await executePerCall(code, cwd, executionOptions);
|
|
1495
|
+
return await executePerCall(code, cwd, executionOptions, tracked.request);
|
|
851
1496
|
}
|
|
852
|
-
return await executeOnSession(code, cwd, executionOptions);
|
|
1497
|
+
return await executeOnSession(code, cwd, executionOptions, tracked.request);
|
|
853
1498
|
} catch (err) {
|
|
854
|
-
if (
|
|
1499
|
+
if (err instanceof PythonKernelStartError || err instanceof PythonKernelCleanupError) throw err;
|
|
1500
|
+
if (isCancellationError(err)) {
|
|
855
1501
|
return createCancelledPythonResult(isTimedOutCancellation(err, executionOptions.signal));
|
|
856
1502
|
}
|
|
857
1503
|
throw err;
|
|
858
1504
|
} finally {
|
|
859
|
-
|
|
860
|
-
if (combinedController) {
|
|
861
|
-
options?.signal?.removeEventListener("abort", forwardAbort);
|
|
862
|
-
deadlineController?.signal.removeEventListener("abort", forwardDeadlineAbort);
|
|
863
|
-
}
|
|
1505
|
+
tracked.request.finish();
|
|
864
1506
|
}
|
|
865
1507
|
}
|