@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.
Files changed (302) hide show
  1. package/CHANGELOG.md +318 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +6 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. package/src/web/search/providers/codex.ts +3 -2
@@ -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
- async function shutdownOrRetainKernel(kernel: PythonKernel): Promise<void> {
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
- requireRemainingTimeoutMs(options.deadlineMs);
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(session: PythonSession, sessionId: string, ownerId: string | undefined): void {
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
- sessionId,
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
- if (initializing.cancelled) {
392
- await shutdownOrRetainKernel(kernel);
393
- throw initializing.cancelled;
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
- await shutdownOrRetainKernel(kernel);
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: false,
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
- requireRemainingTimeoutMs(options.deadlineMs);
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
- const session = isInitializingSession(existing) ? await existing.promise.catch(() => undefined) : existing;
508
- await session?.kernel.shutdown().catch(() => undefined);
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 async function disposeAllKernelSessions(): Promise<void> {
537
- const all = [...sessions.entries()];
538
- const retiring = [...retiringKernels];
539
- retiringKernels.clear();
540
- for (const [id, session] of all) {
541
- if (sessions.get(id) === session) sessions.delete(id);
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 shutdownTargets = resolved.filter(
550
- (entry): entry is readonly [string, PythonSession] => entry[1] !== undefined,
551
- );
552
- const results = await Promise.allSettled(shutdownTargets.map(([, session]) => session.kernel.shutdown()));
553
- for (let i = 0; i < shutdownTargets.length; i += 1) {
554
- const [id, session] = shutdownTargets[i];
555
- const result = results[i];
556
- if (result.status === "fulfilled" && result.value.confirmed) continue;
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 async function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
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 session of [...sessions.values()]) {
576
- if (isInitializingSession(session) || !session.ownerIds.has(ownerId)) continue;
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
- continue;
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
- for (const session of toShutdown) {
584
- if (sessions.get(session.sessionId) === session) sessions.delete(session.sessionId);
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 results = await Promise.allSettled(toShutdown.map(session => session.kernel.shutdown()));
587
- for (let i = 0; i < toShutdown.length; i += 1) {
588
- const session = toShutdown[i];
589
- const result = results[i];
590
- if (result.status === "fulfilled" && result.value.confirmed) {
591
- session.ownerIds.delete(ownerId);
592
- continue;
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 reason = result.status === "rejected" ? result.reason : "not confirmed";
595
- logger.warn("Python kernel shutdown not confirmed", { sessionId: session.sessionId, reason });
596
- if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, session);
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
- const unregisterBridge =
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
- if (options.signal?.aborted) {
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
- return await executeWithKernel(kernel, code, options);
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 cwd = options?.cwd ?? getProjectDir();
815
- const deadlineMs = getExecutionDeadlineMs(options);
816
- const deadlineController = deadlineMs === undefined ? undefined : new AbortController();
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
- requireRemainingTimeoutMs(deadlineMs);
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
- if (deadlineTimer) clearTimeout(deadlineTimer);
860
- if (combinedController) {
861
- options?.signal?.removeEventListener("abort", forwardAbort);
862
- deadlineController?.signal.removeEventListener("abort", forwardDeadlineAbort);
863
- }
1123
+ tracked.request.finish();
864
1124
  }
865
1125
  }