@code-yeongyu/senpi 2026.7.10 → 2026.7.11

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 (311) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +4 -2
  3. package/dist/core/extensions/builtin/bash-timeout/timeout.js +2 -2
  4. package/dist/core/extensions/builtin/bash-timeout/timeout.js.map +1 -1
  5. package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
  6. package/dist/core/extensions/builtin/compaction/index.js +10 -1
  7. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  8. package/dist/core/extensions/builtin/goal/store.d.ts.map +1 -1
  9. package/dist/core/extensions/builtin/goal/store.js +88 -3
  10. package/dist/core/extensions/builtin/goal/store.js.map +1 -1
  11. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.d.ts +3 -0
  12. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.d.ts.map +1 -0
  13. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +83 -0
  14. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js.map +1 -0
  15. package/dist/core/extensions/builtin/prompt-preset/presets.d.ts.map +1 -1
  16. package/dist/core/extensions/builtin/prompt-preset/presets.js +6 -0
  17. package/dist/core/extensions/builtin/prompt-preset/presets.js.map +1 -1
  18. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  19. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  21. package/dist/core/extensions/builtin/prompt-preset/settings.js.map +1 -1
  22. package/dist/core/extensions/builtin/terminal/notify.d.ts +3 -3
  23. package/dist/core/extensions/builtin/terminal/notify.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/terminal/notify.js +7 -4
  25. package/dist/core/extensions/builtin/terminal/notify.js.map +1 -1
  26. package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +5 -3
  27. package/dist/core/extensions/builtin/terminal/runtime-session.d.ts.map +1 -1
  28. package/dist/core/extensions/builtin/terminal/runtime-session.js +32 -16
  29. package/dist/core/extensions/builtin/terminal/runtime-session.js.map +1 -1
  30. package/dist/core/extensions/builtin/terminal/tools/bash-output.d.ts +1 -1
  31. package/dist/core/extensions/builtin/terminal/tools/bash-output.d.ts.map +1 -1
  32. package/dist/core/extensions/builtin/terminal/tools/bash-output.js +10 -4
  33. package/dist/core/extensions/builtin/terminal/tools/bash-output.js.map +1 -1
  34. package/dist/core/extensions/builtin/todotools/tools/todowrite.d.ts.map +1 -1
  35. package/dist/core/extensions/builtin/todotools/tools/todowrite.js +5 -8
  36. package/dist/core/extensions/builtin/todotools/tools/todowrite.js.map +1 -1
  37. package/dist/core/extensions/runner.d.ts +1 -0
  38. package/dist/core/extensions/runner.d.ts.map +1 -1
  39. package/dist/core/extensions/runner.js +5 -0
  40. package/dist/core/extensions/runner.js.map +1 -1
  41. package/dist/core/extensions/types.d.ts +5 -8
  42. package/dist/core/extensions/types.d.ts.map +1 -1
  43. package/dist/core/extensions/types.js.map +1 -1
  44. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  45. package/dist/core/extensions/wrapper.js +22 -3
  46. package/dist/core/extensions/wrapper.js.map +1 -1
  47. package/dist/core/keybindings.d.ts +8 -3
  48. package/dist/core/keybindings.d.ts.map +1 -1
  49. package/dist/core/keybindings.js +7 -3
  50. package/dist/core/keybindings.js.map +1 -1
  51. package/dist/core/model-registry.d.ts +1 -6
  52. package/dist/core/model-registry.d.ts.map +1 -1
  53. package/dist/core/model-registry.js +19 -6
  54. package/dist/core/model-registry.js.map +1 -1
  55. package/dist/core/settings-manager.d.ts +4 -3
  56. package/dist/core/settings-manager.d.ts.map +1 -1
  57. package/dist/core/settings-manager.js +2 -1
  58. package/dist/core/settings-manager.js.map +1 -1
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/modes/interactive/components/custom-editor.js +1 -1
  64. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  65. package/dist/modes/interactive/components/login-dialog.d.ts +4 -0
  66. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/login-dialog.js +9 -2
  68. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  69. package/dist/modes/interactive/components/settings-selector.js +1 -1
  70. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  71. package/dist/modes/interactive/components/thinking-selector.js +1 -1
  72. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  73. package/dist/modes/interactive/components/tool-execution-fallback.d.ts +6 -0
  74. package/dist/modes/interactive/components/tool-execution-fallback.d.ts.map +1 -0
  75. package/dist/modes/interactive/components/tool-execution-fallback.js +40 -0
  76. package/dist/modes/interactive/components/tool-execution-fallback.js.map +1 -0
  77. package/dist/modes/interactive/components/tool-execution-images.d.ts +23 -0
  78. package/dist/modes/interactive/components/tool-execution-images.d.ts.map +1 -0
  79. package/dist/modes/interactive/components/tool-execution-images.js +104 -0
  80. package/dist/modes/interactive/components/tool-execution-images.js.map +1 -0
  81. package/dist/modes/interactive/components/tool-execution-renderer.d.ts +28 -0
  82. package/dist/modes/interactive/components/tool-execution-renderer.d.ts.map +1 -0
  83. package/dist/modes/interactive/components/tool-execution-renderer.js +135 -0
  84. package/dist/modes/interactive/components/tool-execution-renderer.js.map +1 -0
  85. package/dist/modes/interactive/components/tool-execution-types.d.ts +23 -0
  86. package/dist/modes/interactive/components/tool-execution-types.d.ts.map +1 -0
  87. package/dist/modes/interactive/components/tool-execution-types.js +2 -0
  88. package/dist/modes/interactive/components/tool-execution-types.js.map +1 -0
  89. package/dist/modes/interactive/components/tool-execution.d.ts +13 -39
  90. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  91. package/dist/modes/interactive/components/tool-execution.js +51 -294
  92. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  93. package/dist/modes/interactive/components/tool-renderer-boundary.d.ts +17 -0
  94. package/dist/modes/interactive/components/tool-renderer-boundary.d.ts.map +1 -0
  95. package/dist/modes/interactive/components/tool-renderer-boundary.js +75 -0
  96. package/dist/modes/interactive/components/tool-renderer-boundary.js.map +1 -0
  97. package/dist/modes/interactive/components/tree-selector.d.ts +5 -0
  98. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  99. package/dist/modes/interactive/components/tree-selector.js +50 -17
  100. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  101. package/dist/modes/interactive/interactive-mode.d.ts +1 -2
  102. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  103. package/dist/modes/interactive/interactive-mode.js +44 -40
  104. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  105. package/dist/modes/interactive/theme/dark.json +1 -0
  106. package/dist/modes/interactive/theme/light.json +1 -0
  107. package/dist/modes/interactive/theme/theme-schema.json +6 -2
  108. package/dist/modes/interactive/theme/theme.d.ts +3 -2
  109. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  110. package/dist/modes/interactive/theme/theme.js +11 -4
  111. package/dist/modes/interactive/theme/theme.js.map +1 -1
  112. package/dist/utils/clipboard-native.d.ts +1 -0
  113. package/dist/utils/clipboard-native.d.ts.map +1 -1
  114. package/dist/utils/clipboard-native.js.map +1 -1
  115. package/dist/utils/clipboard.d.ts +2 -0
  116. package/dist/utils/clipboard.d.ts.map +1 -1
  117. package/dist/utils/clipboard.js +13 -0
  118. package/dist/utils/clipboard.js.map +1 -1
  119. package/docs/custom-provider.md +4 -3
  120. package/docs/extensions.md +140 -1
  121. package/docs/keybindings.md +1 -0
  122. package/docs/models.md +45 -4
  123. package/docs/providers.md +3 -0
  124. package/docs/quickstart.md +1 -1
  125. package/docs/rpc.md +2 -2
  126. package/docs/sdk.md +1 -1
  127. package/docs/settings.md +3 -3
  128. package/docs/themes.md +5 -3
  129. package/docs/tui.md +1 -1
  130. package/docs/usage.md +2 -1
  131. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  132. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  133. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  134. package/examples/extensions/gondolin/package-lock.json +2 -2
  135. package/examples/extensions/gondolin/package.json +1 -1
  136. package/examples/extensions/sandbox/package-lock.json +2 -2
  137. package/examples/extensions/sandbox/package.json +1 -1
  138. package/examples/extensions/with-deps/package-lock.json +2 -2
  139. package/examples/extensions/with-deps/package.json +1 -1
  140. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +3 -0
  141. package/node_modules/@code-yeongyu/senpi-codemode/package.json +3 -3
  142. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +7 -7
  143. package/node_modules/@code-yeongyu/senpi-codemode/src/completion/tool-bridge.ts +3 -0
  144. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +69 -8
  145. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +39 -13
  146. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +191 -148
  147. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker-entry.js +23 -0
  148. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +4 -175
  149. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-contract.ts +38 -0
  150. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/run-queue.ts +82 -0
  151. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +7 -3
  152. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-host.ts +117 -0
  153. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel-contract.ts +32 -0
  154. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +231 -199
  155. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/process.ts +119 -0
  156. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +237 -0
  157. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-contract.ts +22 -0
  158. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +214 -189
  159. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-process.ts +174 -0
  160. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-queue.ts +100 -0
  161. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-run.ts +98 -0
  162. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-handler.ts +205 -0
  163. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +208 -203
  164. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +215 -25
  165. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +1 -0
  166. package/node_modules/@earendil-works/pi-agent-core/README.md +1 -1
  167. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +2 -0
  168. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  169. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +4 -3
  170. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
  171. package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
  172. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  173. package/node_modules/@earendil-works/pi-ai/README.md +4 -2
  174. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +89 -40
  176. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.d.ts +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.d.ts.map +1 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js.map +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +7 -7
  182. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js.map +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts +1 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts.map +1 -1
  185. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +10 -3
  186. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts +1 -1
  188. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  189. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +5 -1
  190. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  191. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts +3 -1
  192. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts.map +1 -1
  193. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +35 -4
  194. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
  195. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts +1 -1
  196. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
  197. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +7 -3
  198. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
  199. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.d.ts.map +1 -1
  200. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js +8 -3
  201. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js.map +1 -1
  202. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.d.ts.map +1 -1
  203. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js +4 -3
  204. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js.map +1 -1
  205. package/node_modules/@earendil-works/pi-ai/dist/compat.d.ts.map +1 -1
  206. package/node_modules/@earendil-works/pi-ai/dist/compat.js +2 -1
  207. package/node_modules/@earendil-works/pi-ai/dist/compat.js.map +1 -1
  208. package/node_modules/@earendil-works/pi-ai/dist/models.d.ts.map +1 -1
  209. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +483 -61
  210. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  211. package/node_modules/@earendil-works/pi-ai/dist/models.js +13 -4
  212. package/node_modules/@earendil-works/pi-ai/dist/models.js.map +1 -1
  213. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.d.ts.map +1 -1
  214. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +5 -1
  215. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  216. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts +62 -5
  217. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
  218. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +32 -20
  219. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js.map +1 -1
  220. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.d.ts +11 -1
  221. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.d.ts.map +1 -1
  222. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js +6 -4
  223. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js.map +1 -1
  224. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.d.ts +7 -8
  225. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
  226. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.js +20 -21
  227. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.js.map +1 -1
  228. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.d.ts +11 -1
  229. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
  230. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js +6 -4
  231. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
  232. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.d.ts.map +1 -1
  233. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.js +9 -6
  234. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.js.map +1 -1
  235. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.d.ts +2 -2
  236. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.d.ts.map +1 -1
  237. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.js +2 -2
  238. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.js.map +1 -1
  239. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.d.ts +2 -2
  240. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.d.ts.map +1 -1
  241. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js +2 -2
  242. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js.map +1 -1
  243. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts +100 -2
  244. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts.map +1 -1
  245. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js +64 -5
  246. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js.map +1 -1
  247. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.d.ts +56 -0
  248. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.d.ts.map +1 -1
  249. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js +20 -9
  250. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js.map +1 -1
  251. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.d.ts +77 -8
  252. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.d.ts.map +1 -1
  253. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.js +37 -24
  254. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.js.map +1 -1
  255. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.d.ts +3 -3
  256. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.d.ts.map +1 -1
  257. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +3 -3
  258. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js.map +1 -1
  259. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.d.ts +80 -4
  260. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.d.ts.map +1 -1
  261. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js +64 -8
  262. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js.map +1 -1
  263. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +25 -22
  264. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
  265. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +64 -79
  266. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
  267. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts +45 -1
  268. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
  269. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +43 -7
  270. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  271. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.d.ts +1 -1
  272. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.d.ts.map +1 -1
  273. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.js +1 -1
  274. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.js.map +1 -1
  275. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.d.ts +1 -1
  276. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.d.ts.map +1 -1
  277. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.js +1 -1
  278. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.js.map +1 -1
  279. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +30 -7
  280. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  281. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  282. package/node_modules/@earendil-works/pi-ai/dist/utils/deferred-tools.d.ts +9 -0
  283. package/node_modules/@earendil-works/pi-ai/dist/utils/deferred-tools.d.ts.map +1 -0
  284. package/node_modules/@earendil-works/pi-ai/dist/utils/deferred-tools.js +36 -0
  285. package/node_modules/@earendil-works/pi-ai/dist/utils/deferred-tools.js.map +1 -0
  286. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.d.ts +3 -3
  287. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.d.ts.map +1 -1
  288. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +35 -14
  289. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js.map +1 -1
  290. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  291. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  292. package/node_modules/@earendil-works/pi-tui/dist/components/image.d.ts.map +1 -1
  293. package/node_modules/@earendil-works/pi-tui/dist/components/image.js +2 -1
  294. package/node_modules/@earendil-works/pi-tui/dist/components/image.js.map +1 -1
  295. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +2 -1
  296. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -1
  297. package/node_modules/@earendil-works/pi-tui/dist/index.js +2 -1
  298. package/node_modules/@earendil-works/pi-tui/dist/index.js.map +1 -1
  299. package/node_modules/@earendil-works/pi-tui/dist/keys.d.ts.map +1 -1
  300. package/node_modules/@earendil-works/pi-tui/dist/keys.js +6 -5
  301. package/node_modules/@earendil-works/pi-tui/dist/keys.js.map +1 -1
  302. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts +0 -1
  303. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts.map +1 -1
  304. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +0 -9
  305. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js.map +1 -1
  306. package/node_modules/@earendil-works/pi-tui/dist/terminal-text.d.ts +4 -0
  307. package/node_modules/@earendil-works/pi-tui/dist/terminal-text.d.ts.map +1 -0
  308. package/node_modules/@earendil-works/pi-tui/dist/terminal-text.js +18 -0
  309. package/node_modules/@earendil-works/pi-tui/dist/terminal-text.js.map +1 -0
  310. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  311. package/package.json +6 -6
@@ -1,59 +1,24 @@
1
- import { spawn } from "node:child_process";
2
- import { dirname, join } from "node:path";
3
- import type { Readable, Writable } from "node:stream";
4
- import { fileURLToPath } from "node:url";
5
- import {
6
- type BridgeConnectionConfig,
7
- decodeBridgeFrame,
8
- encodeBridgeFrame,
9
- type HostToKernelMessage,
10
- type KernelToHostMessage,
11
- } from "../../bridge/protocol.ts";
1
+ import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
2
+ import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
12
3
 
13
- export interface KernelChild {
14
- readonly stdin: Writable;
15
- readonly stdout: Readable;
16
- readonly stderr: Readable;
17
- readonly pid?: number;
18
- readonly killed: boolean;
19
- kill(signal?: NodeJS.Signals): boolean;
20
- on(event: string, listener: (...args: unknown[]) => void): this;
21
- once(event: string, listener: (...args: unknown[]) => void): this;
22
- off(event: string, listener: (...args: unknown[]) => void): this;
23
- }
4
+ export type { PythonKernelRunOptions, PythonKernelStartOptions } from "./kernel-contract.ts";
5
+ export type { KernelChild, KernelSpawnOptions, KernelSpawnProcess } from "./process.ts";
24
6
 
25
- export interface KernelSpawnOptions {
26
- readonly command: string;
27
- readonly args: readonly string[];
28
- readonly cwd: string;
29
- readonly env: NodeJS.ProcessEnv;
30
- }
31
- export type KernelSpawnProcess = (options: KernelSpawnOptions) => KernelChild;
32
- export interface PythonKernelStartOptions {
33
- readonly interpreterPath: string;
34
- readonly sessionId: string;
35
- readonly cwd: string;
36
- readonly connection: BridgeConnectionConfig;
37
- readonly env?: NodeJS.ProcessEnv;
38
- readonly startupTimeoutMs?: number;
39
- readonly onMessage?: (message: KernelToHostMessage) => void;
40
- readonly spawnProcess?: KernelSpawnProcess;
41
- }
42
- export interface PythonKernelRunOptions {
43
- readonly cellId: string;
44
- readonly code: string;
45
- readonly timeoutMs?: number;
46
- }
47
- type ResultMessage = Extract<KernelToHostMessage, { type: "result" }>;
48
7
  const startupTimeoutMs = 5_000;
8
+ const interruptEscalationMs = 5_000;
49
9
 
50
10
  export class PythonKernel {
51
11
  readonly #options: PythonKernelStartOptions;
52
- #child: KernelChild | null = null;
53
- #stdoutBuffer = "";
54
- #stderrTail = "";
55
- #settleReady: ((error?: Error) => void) | null = null;
56
- #pending = new Map<string, (result: ResultMessage) => void>();
12
+ #transport: PythonKernelTransport | null = null;
13
+ #pending = new Map<string, PendingRun>();
14
+ #queue: PendingRun[] = [];
15
+ #active: PendingRun | null = null;
16
+ #starting: Promise<void> | null = null;
17
+ #retirement: Promise<void> | null = null;
18
+ #closePromise: Promise<void> | null = null;
19
+ #failure: Error | null = null;
20
+ #generation = 0;
21
+ #closed = false;
57
22
 
58
23
  private constructor(options: PythonKernelStartOptions) {
59
24
  this.#options = options;
@@ -61,197 +26,264 @@ export class PythonKernel {
61
26
 
62
27
  static async start(options: PythonKernelStartOptions): Promise<PythonKernel> {
63
28
  const kernel = new PythonKernel(options);
64
- await kernel.#spawn();
29
+ await kernel.#spawn(kernel.#generation);
65
30
  return kernel;
66
31
  }
67
32
 
68
- async run(options: PythonKernelRunOptions): Promise<ResultMessage> {
69
- await this.#ensureStarted();
70
- return await new Promise<ResultMessage>((resolve) => {
71
- const timer = setTimeout(() => {
72
- this.#pending.delete(options.cellId);
73
- void this.#kill();
74
- resolve({
75
- type: "result",
76
- cellId: options.cellId,
77
- ok: false,
78
- error: { message: `Python kernel timed out after ${options.timeoutMs ?? 0}ms` },
79
- durationMs: options.timeoutMs ?? 0,
80
- });
81
- }, options.timeoutMs ?? 30_000);
82
- this.#pending.set(options.cellId, (result) => {
83
- clearTimeout(timer);
84
- resolve(result);
85
- });
86
- this.#write({ type: "run", cellId: options.cellId, code: options.code, timeoutMs: options.timeoutMs });
33
+ run(input: PythonKernelRunOptions): Promise<ResultMessage> {
34
+ if (this.#failure) return Promise.reject(this.#failure);
35
+ if (this.#closed) return Promise.reject(new Error("Python kernel is closed"));
36
+ return new Promise<ResultMessage>((resolve, reject) => {
37
+ const pending: PendingRun = { input, resolve, reject, startedAt: null, timeoutTimer: null };
38
+ this.#pending.set(input.cellId, pending);
39
+ this.#queue.push(pending);
40
+ this.#startNext();
87
41
  });
88
42
  }
89
43
 
90
44
  async interrupt(reason = "interrupted"): Promise<void> {
91
- if (!this.#child) return;
92
- this.#write({ type: "interrupt", reason });
93
- if (process.platform === "win32") this.#child.kill();
94
- else this.#child.kill("SIGINT");
45
+ if (this.#failure) throw this.#failure;
46
+ for (const pending of [...this.#queue]) {
47
+ pending.interruptReason = reason;
48
+ this.#settleRun(pending, failedPythonResult(pending.input.cellId, "Eval interrupted"));
49
+ }
50
+ const active = this.#active;
51
+ const transport = this.#transport;
52
+ if (!active || !transport || active.interruptReason !== undefined) return;
53
+ active.interruptReason = reason;
54
+ if (active.timeoutTimer) clearTimeout(active.timeoutTimer);
55
+ active.timeoutTimer = null;
56
+ active.escalationTimer = setTimeout(
57
+ () => void this.#escalateInterruptedRun(active).catch(() => undefined),
58
+ interruptEscalationMs,
59
+ );
60
+ transport.interrupt(reason);
95
61
  }
96
62
 
97
63
  async reset(): Promise<void> {
98
- await this.#kill();
99
- await this.#spawn();
64
+ if (this.#failure) throw this.#failure;
65
+ if (this.#closed) throw new Error("Python kernel is closed");
66
+ const generation = ++this.#generation;
67
+ const prior = this.#starting;
68
+ const operation = (async () => {
69
+ await prior?.catch(() => undefined);
70
+ if (this.#failure) throw this.#failure;
71
+ if (this.#closed || generation !== this.#generation) throw new Error("Python kernel reset was superseded");
72
+ const transport = this.#transport;
73
+ if (transport) await this.#beginRetirement(transport);
74
+ if (this.#closed || generation !== this.#generation) throw new Error("Python kernel reset was superseded");
75
+ await this.#spawn(generation);
76
+ })();
77
+ this.#starting = operation;
78
+ this.#settleAllPending("Python kernel reset");
79
+ try {
80
+ await operation;
81
+ } finally {
82
+ this.#finishStarting(operation);
83
+ }
100
84
  }
101
85
 
102
86
  deliverToolReply(): void {}
103
87
 
104
88
  async close(): Promise<void> {
105
- const child = this.#child;
106
- if (!child) return;
107
- this.#write({ type: "close" });
108
- await Promise.race([onceExit(child), delay(500)]);
109
- if (!child.killed) await this.#kill();
110
- this.#child = null;
89
+ if (this.#closePromise) return await this.#closePromise;
90
+ this.#closed = true;
91
+ this.#generation += 1;
92
+ this.#settleAllPending("Python kernel closed");
93
+ const starting = this.#starting;
94
+ this.#closePromise = (async () => {
95
+ await starting?.catch(() => undefined);
96
+ await this.#retirement?.catch(() => undefined);
97
+ const transport = this.#transport;
98
+ if (!transport) return;
99
+ try {
100
+ await transport.close();
101
+ } catch (error) {
102
+ if (error instanceof Error) throw this.#recordFailure(error);
103
+ throw error;
104
+ }
105
+ if (this.#transport === transport) this.#transport = null;
106
+ })();
107
+ return await this.#closePromise;
111
108
  }
112
109
 
113
- async #ensureStarted(): Promise<void> {
114
- if (this.#child && !this.#child.killed) return;
115
- await this.#spawn();
110
+ #startNext(): void {
111
+ if (
112
+ this.#closed ||
113
+ this.#failure ||
114
+ this.#active ||
115
+ this.#starting ||
116
+ this.#retirement ||
117
+ this.#queue.length === 0
118
+ )
119
+ return;
120
+ const starting = this.#activateNext();
121
+ this.#starting = starting;
122
+ void starting.then(
123
+ () => this.#finishStarting(starting),
124
+ (error: unknown) => {
125
+ this.#rejectAllPending(error);
126
+ this.#finishStarting(starting);
127
+ },
128
+ );
116
129
  }
117
130
 
118
- async #spawn(): Promise<void> {
119
- const scriptPath = join(dirname(fileURLToPath(import.meta.url)), "prelude.py");
120
- const invocation = splitCommand(this.#options.interpreterPath);
121
- const spawnOptions: KernelSpawnOptions = {
122
- command: invocation.command,
123
- args: [...invocation.args, "-u", scriptPath],
124
- cwd: this.#options.cwd,
125
- env: { ...process.env, ...this.#options.env, PYTHONUNBUFFERED: "1", PYTHONIOENCODING: "utf-8" },
126
- };
127
- const child = (this.#options.spawnProcess ?? defaultSpawn)(spawnOptions);
128
- this.#child = child;
129
- this.#stdoutBuffer = "";
130
- this.#stderrTail = "";
131
- const ready = new Promise<void>((resolve, reject) => {
132
- this.#settleReady = (error) => (error ? reject(error) : resolve());
133
- });
134
- child.stdout.on("data", (chunk) => this.#onStdout(String(chunk)));
135
- child.stderr.on("data", (chunk) => this.#onStderr(String(chunk)));
136
- child.once("exit", (code, signal) => this.#onExit(numberOrNull(code), signalOrNull(signal)));
137
- this.#write({ type: "init", sessionId: this.#options.sessionId, connection: this.#options.connection });
138
- await withTimeout(
139
- ready,
140
- this.#options.startupTimeoutMs ?? startupTimeoutMs,
141
- "Python kernel did not become ready",
142
- );
131
+ async #activateNext(): Promise<void> {
132
+ await this.#ensureStarted();
133
+ if (this.#closed || this.#active || this.#retirement) return;
134
+ const pending = this.#queue.shift();
135
+ if (!pending || !this.#pending.has(pending.input.cellId)) return;
136
+ this.#active = pending;
137
+ pending.startedAt = performance.now();
138
+ const timeoutMs = pending.input.timeoutMs ?? 30_000;
139
+ pending.timeoutTimer = setTimeout(() => this.#timeoutRun(pending, timeoutMs), timeoutMs);
140
+ try {
141
+ this.#transport?.run(pending.input);
142
+ } catch (error) {
143
+ const failure = error instanceof Error ? error : new Error(String(error));
144
+ this.#rejectRun(pending, failure);
145
+ }
143
146
  }
144
147
 
145
- #write(message: HostToKernelMessage): void {
146
- this.#child?.stdin.write(encodeBridgeFrame(message));
148
+ #finishStarting(starting: Promise<void>): void {
149
+ if (this.#starting === starting) this.#starting = null;
150
+ this.#startNext();
147
151
  }
148
152
 
149
- #onStdout(chunk: string): void {
150
- this.#stdoutBuffer += chunk;
151
- let newline = this.#stdoutBuffer.indexOf("\n");
152
- while (newline >= 0) {
153
- const line = this.#stdoutBuffer.slice(0, newline + 1);
154
- this.#stdoutBuffer = this.#stdoutBuffer.slice(newline + 1);
155
- this.#handleLine(line);
156
- newline = this.#stdoutBuffer.indexOf("\n");
157
- }
153
+ #timeoutRun(pending: PendingRun, timeoutMs: number): void {
154
+ if (this.#active !== pending) return;
155
+ if (this.#transport) void this.#beginRetirement(this.#transport).catch(() => undefined);
156
+ this.#settleRun(
157
+ pending,
158
+ failedPythonResult(pending.input.cellId, `Python kernel timed out after ${timeoutMs}ms`),
159
+ );
158
160
  }
159
161
 
160
- #onStderr(chunk: string): void {
161
- this.#stderrTail = `${this.#stderrTail}${chunk}`.slice(-4_000);
162
- this.#emit({ type: "text", stream: "stderr", data: chunk });
162
+ async #ensureStarted(): Promise<void> {
163
+ await this.#retirement;
164
+ if (this.#failure) throw this.#failure;
165
+ if (this.#transport) return;
166
+ await this.#spawn(this.#generation);
163
167
  }
164
168
 
165
- #handleLine(line: string): void {
166
- const decoded = decodeBridgeFrame(line);
167
- if (!decoded.ok) {
168
- this.#emit({ type: "text", stream: "stderr", data: `${decoded.error.message}\n` });
169
- return;
170
- }
171
- const message = decoded.message as KernelToHostMessage;
172
- if (message.type === "ready") this.#settleReady?.();
173
- else if (message.type === "init-failed") this.#settleReady?.(new Error(message.error.message));
174
- else if (message.type === "result") this.#pending.get(message.cellId)?.(message);
175
- this.#emit(message);
169
+ async #spawn(generation: number): Promise<void> {
170
+ if (this.#closed || generation !== this.#generation) throw new Error("Python kernel startup was superseded");
171
+ this.#transport = await PythonKernelTransport.start({
172
+ ...this.#options,
173
+ startupTimeoutMs: this.#options.startupTimeoutMs ?? startupTimeoutMs,
174
+ isOwned: () => !this.#closed && generation === this.#generation,
175
+ onRetirementFailure: (transport, error) => {
176
+ if (!this.#transport) this.#transport = transport;
177
+ this.#recordFailure(error);
178
+ },
179
+ onResult: (transport, result) => this.#onResult(transport, result),
180
+ onError: (transport, error) => this.#onError(transport, error),
181
+ onExit: (transport, error) => this.#onExit(transport, error),
182
+ });
176
183
  }
177
184
 
178
- #onExit(code: number | null, signal: NodeJS.Signals | null): void {
179
- const error = new Error(this.#stderrTail.trim() || `Python kernel exited (${code ?? signal ?? "unknown"})`);
180
- this.#settleReady?.(error);
181
- for (const [cellId, settle] of this.#pending) {
182
- settle({
183
- type: "result",
184
- cellId,
185
- ok: false,
186
- error: { message: "Python kernel died", stack: error.message },
187
- durationMs: 0,
188
- });
189
- }
190
- this.#pending.clear();
191
- this.#child = null;
185
+ #onResult(transport: PythonKernelTransport, result: ResultMessage): void {
186
+ if (this.#transport !== transport) return;
187
+ const pending = this.#pending.get(result.cellId);
188
+ if (pending) this.#settleRun(pending, result);
192
189
  }
193
190
 
194
- #emit(message: KernelToHostMessage): void {
195
- this.#options.onMessage?.(message);
191
+ #onExit(transport: PythonKernelTransport, error: Error): void {
192
+ if (this.#transport !== transport) return;
193
+ this.#transport = null;
194
+ const active = this.#active;
195
+ if (active) this.#settleRun(active, failedPythonResult(active.input.cellId, "Python kernel died", error.message));
196
+ this.#startNext();
196
197
  }
197
198
 
198
- async #kill(): Promise<void> {
199
- const child = this.#child;
200
- if (!child) return;
201
- if (child.pid !== undefined && process.platform !== "win32") {
202
- try {
203
- process.kill(-child.pid, "SIGKILL");
204
- } catch {
205
- child.kill("SIGKILL");
206
- }
199
+ #onError(transport: PythonKernelTransport, error: Error): void {
200
+ if (this.#transport !== transport) return;
201
+ const retirement = this.#beginRetirement(transport);
202
+ void retirement.then(
203
+ () => undefined,
204
+ (retirementError: unknown) => {
205
+ this.#recordFailure(
206
+ retirementError instanceof Error ? retirementError : new Error(String(retirementError)),
207
+ );
208
+ },
209
+ );
210
+ const active = this.#active;
211
+ if (active) this.#settleRun(active, failedPythonResult(active.input.cellId, "Python kernel died", error.message));
212
+ }
213
+
214
+ #settleRun(pending: PendingRun, result: ResultMessage): void {
215
+ if (!this.#pending.delete(pending.input.cellId)) return;
216
+ this.#removePending(pending);
217
+ const durationMs = pending.startedAt === null ? 0 : Math.max(0, performance.now() - pending.startedAt);
218
+ if (pending.interruptReason !== undefined) {
219
+ const message =
220
+ pending.interruptReason === "Eval interrupted"
221
+ ? "Eval interrupted"
222
+ : `Eval interrupted: ${pending.interruptReason}`;
223
+ const error = result.ok ? { message } : { ...result.error, message };
224
+ pending.resolve({ type: "result", cellId: pending.input.cellId, ok: false, error, durationMs });
207
225
  } else {
208
- child.kill("SIGKILL");
226
+ pending.resolve(result.durationMs === 0 ? { ...result, durationMs } : result);
209
227
  }
210
- await Promise.race([onceExit(child), delay(500)]);
211
- this.#child = null;
228
+ this.#startNext();
212
229
  }
213
- }
214
230
 
215
- function defaultSpawn(options: KernelSpawnOptions): KernelChild {
216
- return spawn(options.command, [...options.args], {
217
- cwd: options.cwd,
218
- env: options.env,
219
- stdio: "pipe",
220
- detached: process.platform !== "win32",
221
- windowsHide: true,
222
- });
223
- }
231
+ #rejectRun(pending: PendingRun, error: unknown): void {
232
+ if (!this.#pending.delete(pending.input.cellId)) return;
233
+ this.#removePending(pending);
234
+ pending.reject(error);
235
+ this.#startNext();
236
+ }
224
237
 
225
- function splitCommand(commandLine: string): { readonly command: string; readonly args: readonly string[] } {
226
- const [command, ...args] = commandLine.split(" ").filter(Boolean);
227
- if (!command) throw new Error("Python interpreter path is empty");
228
- return { command, args };
229
- }
238
+ #removePending(pending: PendingRun): void {
239
+ if (pending.timeoutTimer) clearTimeout(pending.timeoutTimer);
240
+ if (pending.escalationTimer) clearTimeout(pending.escalationTimer);
241
+ pending.timeoutTimer = null;
242
+ pending.escalationTimer = undefined;
243
+ if (this.#active === pending) this.#active = null;
244
+ const queuedIndex = this.#queue.indexOf(pending);
245
+ if (queuedIndex >= 0) this.#queue.splice(queuedIndex, 1);
246
+ }
230
247
 
231
- function numberOrNull(value: unknown): number | null {
232
- return typeof value === "number" ? value : null;
233
- }
248
+ #settleAllPending(message: string): void {
249
+ for (const pending of [...this.#pending.values()])
250
+ this.#settleRun(pending, failedPythonResult(pending.input.cellId, message));
251
+ }
234
252
 
235
- function signalOrNull(value: unknown): NodeJS.Signals | null {
236
- return typeof value === "string" ? (value as NodeJS.Signals) : null;
237
- }
253
+ #rejectAllPending(error: unknown): void {
254
+ for (const pending of [...this.#pending.values()]) this.#rejectRun(pending, error);
255
+ }
238
256
 
239
- async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
240
- let timer: NodeJS.Timeout | undefined;
241
- const timeout = new Promise<never>((_, reject) => {
242
- timer = setTimeout(() => reject(new Error(message)), timeoutMs);
243
- });
244
- try {
245
- return await Promise.race([promise, timeout]);
246
- } finally {
247
- if (timer) clearTimeout(timer);
257
+ async #escalateInterruptedRun(pending: PendingRun): Promise<void> {
258
+ if (this.#active !== pending || pending.interruptReason === undefined) return;
259
+ const transport = this.#transport;
260
+ if (transport) await this.#beginRetirement(transport);
261
+ if (this.#pending.has(pending.input.cellId))
262
+ this.#settleRun(pending, failedPythonResult(pending.input.cellId, "Eval interrupted"));
248
263
  }
249
- }
250
264
 
251
- async function delay(ms: number): Promise<void> {
252
- await new Promise((resolve) => setTimeout(resolve, ms));
253
- }
265
+ #beginRetirement(transport: PythonKernelTransport): Promise<void> {
266
+ if (this.#retirement) return this.#retirement;
267
+ const operation = (async () => {
268
+ try {
269
+ await transport.retire();
270
+ } catch (error) {
271
+ if (error instanceof Error) throw this.#recordFailure(error);
272
+ throw error;
273
+ }
274
+ if (this.#transport === transport) this.#transport = null;
275
+ })();
276
+ const retirement = operation.finally(() => {
277
+ if (this.#retirement === retirement) this.#retirement = null;
278
+ this.#startNext();
279
+ });
280
+ this.#retirement = retirement;
281
+ return retirement;
282
+ }
254
283
 
255
- async function onceExit(child: KernelChild): Promise<void> {
256
- await new Promise<void>((resolve) => child.once("exit", () => resolve()));
284
+ #recordFailure(error: Error): Error {
285
+ this.#failure = error;
286
+ this.#rejectAllPending(error);
287
+ return error;
288
+ }
257
289
  }
@@ -0,0 +1,119 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { Readable, Writable } from "node:stream";
3
+
4
+ export interface KernelChild {
5
+ readonly stdin: Writable;
6
+ readonly stdout: Readable;
7
+ readonly stderr: Readable;
8
+ readonly pid?: number;
9
+ readonly killed: boolean;
10
+ kill(signal?: NodeJS.Signals): boolean;
11
+ on(event: string, listener: (...args: unknown[]) => void): this;
12
+ once(event: string, listener: (...args: unknown[]) => void): this;
13
+ off(event: string, listener: (...args: unknown[]) => void): this;
14
+ }
15
+
16
+ export interface KernelSpawnOptions {
17
+ readonly command: string;
18
+ readonly args: readonly string[];
19
+ readonly cwd: string;
20
+ readonly env: NodeJS.ProcessEnv;
21
+ }
22
+
23
+ export type KernelSpawnProcess = (options: KernelSpawnOptions) => KernelChild;
24
+
25
+ export class PythonKernelRetirementError extends Error {
26
+ constructor(pid: number | undefined) {
27
+ super(`Python kernel process${pid === undefined ? "" : ` ${pid}`} did not exit after SIGKILL`);
28
+ this.name = "PythonKernelRetirementError";
29
+ }
30
+ }
31
+
32
+ export function defaultSpawn(options: KernelSpawnOptions): KernelChild {
33
+ return spawn(options.command, [...options.args], {
34
+ cwd: options.cwd,
35
+ env: options.env,
36
+ stdio: "pipe",
37
+ detached: process.platform !== "win32",
38
+ windowsHide: true,
39
+ });
40
+ }
41
+
42
+ export function splitCommand(commandLine: string): { readonly command: string; readonly args: readonly string[] } {
43
+ const [command, ...args] = commandLine.split(" ").filter(Boolean);
44
+ if (!command) throw new Error("Python interpreter path is empty");
45
+ return { command, args };
46
+ }
47
+
48
+ export function numberOrNull(value: unknown): number | null {
49
+ return typeof value === "number" ? value : null;
50
+ }
51
+
52
+ export function signalOrNull(value: unknown): string | null {
53
+ return typeof value === "string" ? value : null;
54
+ }
55
+
56
+ export async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
57
+ let timer: NodeJS.Timeout | undefined;
58
+ const timeout = new Promise<never>((_, reject) => {
59
+ timer = setTimeout(() => reject(new Error(message)), timeoutMs);
60
+ });
61
+ try {
62
+ return await Promise.race([promise, timeout]);
63
+ } finally {
64
+ if (timer) clearTimeout(timer);
65
+ }
66
+ }
67
+
68
+ export async function waitForExit(child: KernelChild, timeoutMs: number): Promise<boolean> {
69
+ return await new Promise<boolean>((resolve) => {
70
+ let timer: NodeJS.Timeout | undefined;
71
+ const settle = (exited: boolean) => {
72
+ child.off("exit", onExit);
73
+ if (timer) clearTimeout(timer);
74
+ resolve(exited);
75
+ };
76
+ const onExit = () => settle(true);
77
+ child.on("exit", onExit);
78
+ timer = setTimeout(() => settle(false), timeoutMs);
79
+ timer.unref?.();
80
+ });
81
+ }
82
+
83
+ export async function hardKill(child: KernelChild, timeoutMs: number): Promise<void> {
84
+ await new Promise<void>((resolve, reject) => {
85
+ let timer: NodeJS.Timeout | undefined;
86
+ let settled = false;
87
+ const settle = (error?: Error) => {
88
+ if (settled) return;
89
+ settled = true;
90
+ child.off("exit", onExit);
91
+ if (timer) clearTimeout(timer);
92
+ if (error) reject(error);
93
+ else resolve();
94
+ };
95
+ const onExit = () => settle();
96
+ child.on("exit", onExit);
97
+ let signalDelivered = true;
98
+ if (child.pid !== undefined && process.platform !== "win32") {
99
+ try {
100
+ process.kill(-child.pid, "SIGKILL");
101
+ } catch (error) {
102
+ if (!(error instanceof Error)) {
103
+ settle(new Error(String(error)));
104
+ return;
105
+ }
106
+ signalDelivered = child.kill("SIGKILL");
107
+ }
108
+ } else {
109
+ signalDelivered = child.kill("SIGKILL");
110
+ }
111
+ if (!signalDelivered) {
112
+ settle();
113
+ return;
114
+ }
115
+ if (settled) return;
116
+ timer = setTimeout(() => settle(new PythonKernelRetirementError(child.pid)), timeoutMs);
117
+ timer.unref?.();
118
+ });
119
+ }