@oh-my-pi/pi-coding-agent 8.1.0 → 8.2.0

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 (402) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/docs/session.md +111 -46
  3. package/examples/custom-tools/hello/index.ts +1 -1
  4. package/examples/custom-tools/todo/index.ts +3 -4
  5. package/examples/extensions/api-demo.ts +0 -1
  6. package/examples/extensions/chalk-logger.ts +2 -3
  7. package/examples/extensions/hello.ts +0 -1
  8. package/examples/extensions/pirate.ts +0 -1
  9. package/examples/extensions/plan-mode.ts +15 -16
  10. package/examples/extensions/todo.ts +3 -4
  11. package/examples/extensions/tools.ts +1 -2
  12. package/examples/extensions/with-deps/index.ts +0 -1
  13. package/examples/hooks/auto-commit-on-exit.ts +1 -2
  14. package/examples/hooks/confirm-destructive.ts +0 -1
  15. package/examples/hooks/custom-compaction.ts +1 -2
  16. package/examples/hooks/dirty-repo-guard.ts +0 -1
  17. package/examples/hooks/file-trigger.ts +3 -4
  18. package/examples/hooks/git-checkpoint.ts +0 -1
  19. package/examples/hooks/handoff.ts +3 -4
  20. package/examples/hooks/permission-gate.ts +1 -2
  21. package/examples/hooks/protected-paths.ts +1 -2
  22. package/examples/hooks/qna.ts +2 -3
  23. package/examples/hooks/snake.ts +4 -5
  24. package/examples/hooks/status-line.ts +0 -1
  25. package/examples/sdk/01-minimal.ts +2 -3
  26. package/examples/sdk/02-custom-model.ts +2 -3
  27. package/examples/sdk/03-custom-prompt.ts +3 -4
  28. package/examples/sdk/04-skills.ts +2 -3
  29. package/examples/sdk/06-extensions.ts +1 -2
  30. package/examples/sdk/06-hooks.ts +6 -7
  31. package/examples/sdk/07-context-files.ts +0 -1
  32. package/examples/sdk/08-prompt-templates.ts +0 -1
  33. package/examples/sdk/08-slash-commands.ts +0 -1
  34. package/examples/sdk/09-api-keys-and-oauth.ts +0 -1
  35. package/examples/sdk/10-settings.ts +0 -1
  36. package/examples/sdk/11-sessions.ts +0 -1
  37. package/package.json +51 -23
  38. package/scripts/format-prompts.ts +0 -1
  39. package/src/capability/context-file.ts +2 -3
  40. package/src/capability/extension-module.ts +2 -3
  41. package/src/capability/extension.ts +2 -3
  42. package/src/capability/fs.ts +20 -21
  43. package/src/capability/hook.ts +2 -3
  44. package/src/capability/index.ts +15 -16
  45. package/src/capability/instruction.ts +2 -3
  46. package/src/capability/mcp.ts +2 -3
  47. package/src/capability/prompt.ts +2 -3
  48. package/src/capability/rule.ts +2 -3
  49. package/src/capability/settings.ts +1 -2
  50. package/src/capability/skill.ts +2 -3
  51. package/src/capability/slash-command.ts +2 -3
  52. package/src/capability/ssh.ts +2 -3
  53. package/src/capability/system-prompt.ts +2 -3
  54. package/src/capability/tool.ts +2 -3
  55. package/src/cli/args.ts +5 -6
  56. package/src/cli/config-cli.ts +6 -7
  57. package/src/cli/file-processor.ts +19 -17
  58. package/src/cli/jupyter-cli.ts +105 -0
  59. package/src/cli/list-models.ts +10 -11
  60. package/src/cli/plugin-cli.ts +20 -21
  61. package/src/cli/session-picker.ts +2 -3
  62. package/src/cli/setup-cli.ts +2 -3
  63. package/src/cli/stats-cli.ts +2 -3
  64. package/src/cli/update-cli.ts +25 -22
  65. package/src/commit/agentic/agent.ts +21 -23
  66. package/src/commit/agentic/fallback.ts +9 -9
  67. package/src/commit/agentic/index.ts +30 -38
  68. package/src/commit/agentic/state.ts +1 -6
  69. package/src/commit/agentic/tools/analyze-file.ts +15 -15
  70. package/src/commit/agentic/tools/git-file-diff.ts +3 -3
  71. package/src/commit/agentic/tools/git-hunk.ts +7 -7
  72. package/src/commit/agentic/tools/git-overview.ts +5 -5
  73. package/src/commit/agentic/tools/index.ts +14 -14
  74. package/src/commit/agentic/tools/propose-changelog.ts +6 -6
  75. package/src/commit/agentic/tools/propose-commit.ts +8 -8
  76. package/src/commit/agentic/tools/recent-commits.ts +2 -2
  77. package/src/commit/agentic/tools/split-commit.ts +19 -23
  78. package/src/commit/agentic/topo-sort.ts +1 -1
  79. package/src/commit/agentic/trivial.ts +3 -3
  80. package/src/commit/agentic/validation.ts +12 -12
  81. package/src/commit/analysis/conventional.ts +7 -11
  82. package/src/commit/analysis/index.ts +4 -4
  83. package/src/commit/analysis/scope.ts +4 -4
  84. package/src/commit/analysis/summary.ts +7 -9
  85. package/src/commit/analysis/validation.ts +1 -1
  86. package/src/commit/changelog/detect.ts +6 -6
  87. package/src/commit/changelog/generate.ts +7 -9
  88. package/src/commit/changelog/index.ts +13 -13
  89. package/src/commit/changelog/parse.ts +2 -2
  90. package/src/commit/cli.ts +1 -1
  91. package/src/commit/git/diff.ts +3 -3
  92. package/src/commit/git/index.ts +19 -24
  93. package/src/commit/index.ts +1 -1
  94. package/src/commit/map-reduce/index.ts +9 -9
  95. package/src/commit/map-reduce/map-phase.ts +19 -34
  96. package/src/commit/map-reduce/reduce-phase.ts +9 -11
  97. package/src/commit/message.ts +2 -2
  98. package/src/commit/model-selection.ts +3 -7
  99. package/src/commit/pipeline.ts +20 -22
  100. package/src/commit/utils/exclusions.ts +3 -3
  101. package/src/config/file-lock.ts +17 -7
  102. package/src/config/keybindings.ts +6 -8
  103. package/src/config/model-registry.ts +55 -37
  104. package/src/config/model-resolver.ts +18 -19
  105. package/src/config/prompt-templates.ts +11 -11
  106. package/src/config/settings-manager.ts +50 -34
  107. package/src/config.ts +60 -62
  108. package/src/cursor.ts +11 -9
  109. package/src/discovery/agents-md.ts +11 -12
  110. package/src/discovery/builtin.ts +68 -73
  111. package/src/discovery/claude.ts +41 -42
  112. package/src/discovery/cline.ts +11 -12
  113. package/src/discovery/codex.ts +52 -53
  114. package/src/discovery/cursor.ts +9 -10
  115. package/src/discovery/gemini.ts +17 -22
  116. package/src/discovery/github.ts +13 -14
  117. package/src/discovery/helpers.ts +35 -34
  118. package/src/discovery/index.ts +16 -18
  119. package/src/discovery/mcp-json.ts +8 -9
  120. package/src/discovery/ssh.ts +8 -9
  121. package/src/discovery/vscode.ts +4 -5
  122. package/src/discovery/windsurf.ts +6 -7
  123. package/src/exa/company.ts +1 -2
  124. package/src/exa/index.ts +2 -3
  125. package/src/exa/linkedin.ts +1 -2
  126. package/src/exa/mcp-client.ts +14 -16
  127. package/src/exa/render.ts +10 -11
  128. package/src/exa/researcher.ts +1 -2
  129. package/src/exa/search.ts +1 -2
  130. package/src/exa/types.ts +0 -1
  131. package/src/exa/websets.ts +1 -2
  132. package/src/exec/bash-executor.ts +3 -4
  133. package/src/exec/exec.ts +0 -1
  134. package/src/export/custom-share.ts +5 -6
  135. package/src/export/html/index.ts +24 -21
  136. package/src/export/ttsr.ts +2 -3
  137. package/src/extensibility/custom-commands/bundled/review/index.ts +7 -8
  138. package/src/extensibility/custom-commands/loader.ts +17 -14
  139. package/src/extensibility/custom-commands/types.ts +1 -2
  140. package/src/extensibility/custom-tools/loader.ts +10 -11
  141. package/src/extensibility/custom-tools/types.ts +6 -7
  142. package/src/extensibility/custom-tools/wrapper.ts +2 -3
  143. package/src/extensibility/extensions/loader.ts +75 -53
  144. package/src/extensibility/extensions/runner.ts +11 -12
  145. package/src/extensibility/extensions/types.ts +19 -26
  146. package/src/extensibility/extensions/wrapper.ts +3 -4
  147. package/src/extensibility/hooks/index.ts +1 -1
  148. package/src/extensibility/hooks/loader.ts +8 -9
  149. package/src/extensibility/hooks/runner.ts +7 -8
  150. package/src/extensibility/hooks/tool-wrapper.ts +0 -1
  151. package/src/extensibility/hooks/types.ts +10 -17
  152. package/src/extensibility/plugins/doctor.ts +3 -3
  153. package/src/extensibility/plugins/installer.ts +27 -27
  154. package/src/extensibility/plugins/loader.ts +59 -56
  155. package/src/extensibility/plugins/manager.ts +211 -171
  156. package/src/extensibility/plugins/parser.ts +1 -1
  157. package/src/extensibility/plugins/paths.ts +8 -8
  158. package/src/extensibility/skills.ts +63 -60
  159. package/src/extensibility/slash-commands.ts +10 -10
  160. package/src/index.ts +46 -46
  161. package/src/internal-urls/agent-protocol.ts +21 -11
  162. package/src/internal-urls/artifact-protocol.ts +17 -13
  163. package/src/internal-urls/router.ts +1 -2
  164. package/src/internal-urls/rule-protocol.ts +3 -4
  165. package/src/internal-urls/skill-protocol.ts +3 -4
  166. package/src/ipy/executor.ts +14 -10
  167. package/src/ipy/gateway-coordinator.ts +79 -90
  168. package/src/ipy/kernel.ts +32 -30
  169. package/src/ipy/modules.ts +13 -13
  170. package/src/lsp/client.ts +21 -10
  171. package/src/lsp/clients/biome-client.ts +1 -2
  172. package/src/lsp/clients/index.ts +3 -3
  173. package/src/lsp/clients/lsp-linter-client.ts +4 -5
  174. package/src/lsp/config.ts +15 -15
  175. package/src/lsp/edits.ts +4 -5
  176. package/src/lsp/index.ts +43 -44
  177. package/src/lsp/lspmux.ts +8 -8
  178. package/src/lsp/render.ts +10 -16
  179. package/src/lsp/utils.ts +3 -3
  180. package/src/main.ts +55 -34
  181. package/src/mcp/client.ts +2 -3
  182. package/src/mcp/config.ts +5 -6
  183. package/src/mcp/json-rpc.ts +0 -1
  184. package/src/mcp/loader.ts +3 -4
  185. package/src/mcp/manager.ts +17 -18
  186. package/src/mcp/tool-bridge.ts +4 -9
  187. package/src/mcp/tool-cache.ts +2 -3
  188. package/src/mcp/transports/http.ts +2 -4
  189. package/src/mcp/transports/stdio.ts +1 -2
  190. package/src/migrations.ts +60 -49
  191. package/src/modes/components/armin.ts +4 -5
  192. package/src/modes/components/assistant-message.ts +6 -6
  193. package/src/modes/components/bash-execution.ts +7 -8
  194. package/src/modes/components/bordered-loader.ts +3 -3
  195. package/src/modes/components/branch-summary-message.ts +3 -3
  196. package/src/modes/components/compaction-summary-message.ts +3 -3
  197. package/src/modes/components/countdown-timer.ts +0 -1
  198. package/src/modes/components/custom-message.ts +5 -5
  199. package/src/modes/components/diff.ts +1 -1
  200. package/src/modes/components/dynamic-border.ts +2 -2
  201. package/src/modes/components/extensions/extension-dashboard.ts +6 -7
  202. package/src/modes/components/extensions/extension-list.ts +2 -3
  203. package/src/modes/components/extensions/inspector-panel.ts +3 -4
  204. package/src/modes/components/extensions/state-manager.ts +25 -26
  205. package/src/modes/components/extensions/types.ts +1 -2
  206. package/src/modes/components/footer.ts +47 -43
  207. package/src/modes/components/history-search.ts +2 -2
  208. package/src/modes/components/hook-editor.ts +3 -4
  209. package/src/modes/components/hook-input.ts +2 -3
  210. package/src/modes/components/hook-message.ts +5 -5
  211. package/src/modes/components/hook-selector.ts +2 -3
  212. package/src/modes/components/keybinding-hints.ts +2 -3
  213. package/src/modes/components/login-dialog.ts +2 -2
  214. package/src/modes/components/model-selector.ts +12 -12
  215. package/src/modes/components/oauth-selector.ts +2 -2
  216. package/src/modes/components/plugin-settings.ts +20 -20
  217. package/src/modes/components/python-execution.ts +7 -8
  218. package/src/modes/components/queue-mode-selector.ts +3 -3
  219. package/src/modes/components/read-tool-group.ts +2 -2
  220. package/src/modes/components/session-selector.ts +4 -4
  221. package/src/modes/components/settings-defs.ts +77 -69
  222. package/src/modes/components/settings-selector.ts +16 -16
  223. package/src/modes/components/show-images-selector.ts +2 -2
  224. package/src/modes/components/status-line/segments.ts +4 -4
  225. package/src/modes/components/status-line/separators.ts +1 -1
  226. package/src/modes/components/status-line/types.ts +2 -2
  227. package/src/modes/components/status-line-segment-editor.ts +7 -8
  228. package/src/modes/components/status-line.ts +12 -12
  229. package/src/modes/components/theme-selector.ts +8 -7
  230. package/src/modes/components/thinking-selector.ts +4 -4
  231. package/src/modes/components/todo-display.ts +2 -2
  232. package/src/modes/components/todo-reminder.ts +4 -4
  233. package/src/modes/components/tool-execution.ts +11 -16
  234. package/src/modes/components/tree-selector.ts +11 -11
  235. package/src/modes/components/ttsr-notification.ts +5 -5
  236. package/src/modes/components/user-message-selector.ts +1 -1
  237. package/src/modes/components/user-message.ts +1 -1
  238. package/src/modes/components/visual-truncate.ts +0 -1
  239. package/src/modes/components/welcome.ts +4 -4
  240. package/src/modes/controllers/command-controller.ts +46 -47
  241. package/src/modes/controllers/event-controller.ts +16 -20
  242. package/src/modes/controllers/extension-ui-controller.ts +40 -46
  243. package/src/modes/controllers/input-controller.ts +17 -18
  244. package/src/modes/controllers/selector-controller.ts +103 -91
  245. package/src/modes/index.ts +3 -3
  246. package/src/modes/interactive-mode.ts +27 -29
  247. package/src/modes/print-mode.ts +12 -13
  248. package/src/modes/rpc/rpc-client.ts +7 -8
  249. package/src/modes/rpc/rpc-mode.ts +24 -25
  250. package/src/modes/rpc/rpc-types.ts +3 -4
  251. package/src/modes/theme/mermaid-cache.ts +2 -2
  252. package/src/modes/theme/theme.ts +128 -53
  253. package/src/modes/types.ts +10 -10
  254. package/src/modes/utils/ui-helpers.ts +17 -17
  255. package/src/patch/applicator.ts +18 -19
  256. package/src/patch/diff.ts +1 -2
  257. package/src/patch/fuzzy.ts +1 -2
  258. package/src/patch/index.ts +10 -11
  259. package/src/patch/normalize.ts +4 -4
  260. package/src/patch/normative.ts +1 -2
  261. package/src/patch/parser.ts +8 -9
  262. package/src/patch/shared.ts +12 -13
  263. package/src/sdk.ts +60 -63
  264. package/src/session/agent-session.ts +83 -84
  265. package/src/session/agent-storage.ts +11 -11
  266. package/src/session/artifacts.ts +8 -9
  267. package/src/session/auth-storage.ts +25 -29
  268. package/src/session/compaction/branch-summarization.ts +7 -10
  269. package/src/session/compaction/compaction.ts +8 -19
  270. package/src/session/compaction/utils.ts +6 -9
  271. package/src/session/history-storage.ts +10 -10
  272. package/src/session/messages.ts +4 -5
  273. package/src/session/session-manager.ts +76 -65
  274. package/src/session/session-storage.ts +57 -69
  275. package/src/session/storage-migration.ts +2 -3
  276. package/src/session/streaming-output.ts +2 -2
  277. package/src/ssh/connection-manager.ts +43 -50
  278. package/src/ssh/ssh-executor.ts +2 -2
  279. package/src/ssh/sshfs-mount.ts +11 -18
  280. package/src/system-prompt.ts +27 -34
  281. package/src/task/agents.ts +45 -30
  282. package/src/task/commands.ts +6 -7
  283. package/src/task/discovery.ts +39 -76
  284. package/src/task/executor.ts +14 -15
  285. package/src/task/index.ts +33 -36
  286. package/src/task/output-manager.ts +3 -4
  287. package/src/task/parallel.ts +0 -1
  288. package/src/task/render.ts +19 -20
  289. package/src/task/subprocess-tool-registry.ts +1 -2
  290. package/src/task/worker-protocol.ts +3 -3
  291. package/src/task/worker.ts +32 -38
  292. package/src/task/worktree.ts +19 -19
  293. package/src/tools/ask.ts +8 -9
  294. package/src/tools/bash-interceptor.ts +1 -5
  295. package/src/tools/bash.ts +19 -18
  296. package/src/tools/calculator.ts +12 -12
  297. package/src/tools/complete.ts +3 -4
  298. package/src/tools/context.ts +2 -2
  299. package/src/tools/fetch.ts +23 -26
  300. package/src/tools/find.ts +15 -16
  301. package/src/tools/gemini-image.ts +14 -14
  302. package/src/tools/grep.ts +27 -27
  303. package/src/tools/index.ts +78 -56
  304. package/src/tools/list-limit.ts +1 -1
  305. package/src/tools/ls.ts +7 -7
  306. package/src/tools/notebook.ts +5 -5
  307. package/src/tools/output-meta.ts +3 -4
  308. package/src/tools/output-utils.ts +1 -1
  309. package/src/tools/path-utils.ts +5 -5
  310. package/src/tools/python.ts +36 -37
  311. package/src/tools/read.ts +23 -23
  312. package/src/tools/render-utils.ts +8 -9
  313. package/src/tools/renderers.ts +6 -7
  314. package/src/tools/review.ts +8 -11
  315. package/src/tools/ssh.ts +31 -30
  316. package/src/tools/todo-write.ts +13 -13
  317. package/src/tools/tool-errors.ts +3 -3
  318. package/src/tools/tool-result.ts +3 -8
  319. package/src/tools/write.ts +11 -16
  320. package/src/tui/code-cell.ts +3 -9
  321. package/src/tui/file-list.ts +3 -4
  322. package/src/tui/output-block.ts +1 -2
  323. package/src/tui/status-line.ts +2 -3
  324. package/src/tui/tree-list.ts +2 -3
  325. package/src/tui/types.ts +1 -2
  326. package/src/tui/utils.ts +2 -3
  327. package/src/utils/changelog.ts +9 -10
  328. package/src/utils/clipboard.ts +11 -11
  329. package/src/utils/file-mentions.ts +4 -10
  330. package/src/utils/frontmatter.ts +6 -3
  331. package/src/utils/fuzzy.ts +2 -2
  332. package/src/utils/image-convert.ts +1 -1
  333. package/src/utils/image-resize.ts +1 -1
  334. package/src/utils/mime.ts +2 -2
  335. package/src/utils/shell-snapshot.ts +11 -13
  336. package/src/utils/shell.ts +4 -5
  337. package/src/utils/title-generator.ts +8 -9
  338. package/src/utils/tools-manager.ts +23 -23
  339. package/src/vendor/photon/index.js +1099 -1059
  340. package/src/vendor/photon/photon_rs_bg.wasm +0 -0
  341. package/src/web/scrapers/artifacthub.ts +1 -1
  342. package/src/web/scrapers/arxiv.ts +2 -2
  343. package/src/web/scrapers/bluesky.ts +2 -2
  344. package/src/web/scrapers/cheatsh.ts +1 -1
  345. package/src/web/scrapers/chocolatey.ts +2 -2
  346. package/src/web/scrapers/choosealicense.ts +5 -5
  347. package/src/web/scrapers/cisa-kev.ts +1 -1
  348. package/src/web/scrapers/crossref.ts +2 -2
  349. package/src/web/scrapers/devto.ts +3 -3
  350. package/src/web/scrapers/discogs.ts +3 -4
  351. package/src/web/scrapers/discourse.ts +1 -1
  352. package/src/web/scrapers/dockerhub.ts +1 -1
  353. package/src/web/scrapers/fdroid.ts +2 -2
  354. package/src/web/scrapers/firefox-addons.ts +3 -3
  355. package/src/web/scrapers/flathub.ts +1 -1
  356. package/src/web/scrapers/github.ts +3 -3
  357. package/src/web/scrapers/gitlab.ts +4 -4
  358. package/src/web/scrapers/hackernews.ts +2 -2
  359. package/src/web/scrapers/huggingface.ts +1 -1
  360. package/src/web/scrapers/iacr.ts +2 -2
  361. package/src/web/scrapers/index.ts +0 -1
  362. package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
  363. package/src/web/scrapers/lemmy.ts +2 -2
  364. package/src/web/scrapers/maven.ts +2 -2
  365. package/src/web/scrapers/mdn.ts +2 -4
  366. package/src/web/scrapers/metacpan.ts +2 -2
  367. package/src/web/scrapers/musicbrainz.ts +1 -2
  368. package/src/web/scrapers/npm.ts +1 -1
  369. package/src/web/scrapers/nuget.ts +2 -2
  370. package/src/web/scrapers/nvd.ts +3 -3
  371. package/src/web/scrapers/ollama.ts +7 -9
  372. package/src/web/scrapers/opencorporates.ts +2 -2
  373. package/src/web/scrapers/openlibrary.ts +6 -6
  374. package/src/web/scrapers/orcid.ts +0 -1
  375. package/src/web/scrapers/osv.ts +2 -2
  376. package/src/web/scrapers/packagist.ts +1 -1
  377. package/src/web/scrapers/pubmed.ts +1 -2
  378. package/src/web/scrapers/rawg.ts +2 -2
  379. package/src/web/scrapers/readthedocs.ts +1 -2
  380. package/src/web/scrapers/repology.ts +2 -2
  381. package/src/web/scrapers/rfc.ts +1 -1
  382. package/src/web/scrapers/searchcode.ts +2 -2
  383. package/src/web/scrapers/semantic-scholar.ts +1 -1
  384. package/src/web/scrapers/snapcraft.ts +2 -2
  385. package/src/web/scrapers/sourcegraph.ts +1 -1
  386. package/src/web/scrapers/spdx.ts +3 -3
  387. package/src/web/scrapers/spotify.ts +0 -1
  388. package/src/web/scrapers/twitter.ts +1 -1
  389. package/src/web/scrapers/types.ts +1 -2
  390. package/src/web/scrapers/utils.ts +5 -5
  391. package/src/web/scrapers/wikidata.ts +3 -3
  392. package/src/web/scrapers/youtube.ts +9 -14
  393. package/src/web/search/auth.ts +4 -9
  394. package/src/web/search/index.ts +11 -21
  395. package/src/web/search/providers/anthropic.ts +3 -9
  396. package/src/web/search/providers/exa.ts +6 -10
  397. package/src/web/search/providers/perplexity.ts +5 -5
  398. package/src/web/search/render.ts +16 -18
  399. package/scripts/generate-wasm-b64.ts +0 -24
  400. package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
  401. package/src/task/.executor.ts.kate-swp +0 -0
  402. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +0 -1
@@ -1,4 +1,7 @@
1
- import { shutdownSharedGateway } from "@oh-my-pi/pi-coding-agent/ipy/gateway-coordinator";
1
+ import { logger } from "@oh-my-pi/pi-utils";
2
+ import { OutputSink } from "../session/streaming-output";
3
+ import { time } from "../utils/timings";
4
+ import { shutdownSharedGateway } from "./gateway-coordinator";
2
5
  import {
3
6
  checkPythonKernelAvailability,
4
7
  type KernelDisplayOutput,
@@ -6,9 +9,7 @@ import {
6
9
  type KernelExecuteResult,
7
10
  type PreludeHelper,
8
11
  PythonKernel,
9
- } from "@oh-my-pi/pi-coding-agent/ipy/kernel";
10
- import { OutputSink } from "@oh-my-pi/pi-coding-agent/session/streaming-output";
11
- import { logger } from "@oh-my-pi/pi-utils";
12
+ } from "./kernel";
12
13
 
13
14
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
14
15
  const MAX_KERNEL_SESSIONS = 4;
@@ -112,7 +113,7 @@ async function cleanupIdleSessions(): Promise<void> {
112
113
 
113
114
  if (toDispose.length > 0) {
114
115
  logger.debug("Cleaning up idle kernel sessions", { count: toDispose.length });
115
- await Promise.allSettled(toDispose.map((session) => disposeKernelSession(session)));
116
+ await Promise.allSettled(toDispose.map(session => disposeKernelSession(session)));
116
117
  }
117
118
 
118
119
  if (kernelSessions.size === 0) {
@@ -136,7 +137,7 @@ async function evictOldestSession(): Promise<void> {
136
137
  export async function disposeAllKernelSessions(): Promise<void> {
137
138
  stopCleanupTimer();
138
139
  const sessions = Array.from(kernelSessions.values());
139
- await Promise.allSettled(sessions.map((session) => disposeKernelSession(session)));
140
+ await Promise.allSettled(sessions.map(session => disposeKernelSession(session)));
140
141
  }
141
142
 
142
143
  async function ensureKernelAvailable(cwd: string): Promise<void> {
@@ -154,6 +155,7 @@ export async function warmPythonEnvironment(
154
155
  ): Promise<{ ok: boolean; reason?: string; docs: PreludeHelper[] }> {
155
156
  try {
156
157
  await ensureKernelAvailable(cwd);
158
+ time("warmPython:ensureKernelAvailable");
157
159
  } catch (err: unknown) {
158
160
  const reason = err instanceof Error ? err.message : String(err);
159
161
  cachedPreludeDocs = [];
@@ -167,10 +169,11 @@ export async function warmPythonEnvironment(
167
169
  const docs = await withKernelSession(
168
170
  resolvedSessionId,
169
171
  cwd,
170
- async (kernel) => kernel.introspectPrelude(),
172
+ async kernel => kernel.introspectPrelude(),
171
173
  useSharedGateway,
172
174
  sessionFile,
173
175
  );
176
+ time("warmPython:withKernelSession");
174
177
  cachedPreludeDocs = docs;
175
178
  return { ok: true, docs };
176
179
  } catch (err: unknown) {
@@ -230,6 +233,7 @@ async function createKernelSession(
230
233
  let kernel: PythonKernel;
231
234
  try {
232
235
  kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
236
+ time("createKernelSession:PythonKernel.start");
233
237
  } catch (err) {
234
238
  if (!isRetry && isResourceExhaustionError(err)) {
235
239
  await recoverFromResourceExhaustion();
@@ -362,8 +366,8 @@ async function executeWithKernel(
362
366
  const result = await kernel.execute(code, {
363
367
  signal: options?.signal,
364
368
  timeoutMs: options?.timeoutMs,
365
- onChunk: (text) => sink.push(text),
366
- onDisplay: (output) => void displayOutputs.push(output),
369
+ onChunk: text => sink.push(text),
370
+ onDisplay: output => void displayOutputs.push(output),
367
371
  });
368
372
 
369
373
  if (result.cancelled) {
@@ -447,7 +451,7 @@ export async function executePython(code: string, options?: PythonExecutorOption
447
451
  return await withKernelSession(
448
452
  sessionId,
449
453
  cwd,
450
- async (kernel) => executeWithKernel(kernel, code, options),
454
+ async kernel => executeWithKernel(kernel, code, options),
451
455
  useSharedGateway,
452
456
  sessionFile,
453
457
  artifactsDir,
@@ -1,22 +1,12 @@
1
- import {
2
- closeSync,
3
- existsSync,
4
- mkdirSync,
5
- openSync,
6
- readFileSync,
7
- renameSync,
8
- statSync,
9
- unlinkSync,
10
- utimesSync,
11
- writeFileSync,
12
- } from "node:fs";
1
+ import * as fs from "node:fs";
13
2
  import { createServer } from "node:net";
14
- import { delimiter, join } from "node:path";
15
- import { getAgentDir } from "@oh-my-pi/pi-coding-agent/config";
16
- import { getShellConfig, killProcessTree } from "@oh-my-pi/pi-coding-agent/utils/shell";
17
- import { getOrCreateSnapshot } from "@oh-my-pi/pi-coding-agent/utils/shell-snapshot";
18
- import { logger } from "@oh-my-pi/pi-utils";
3
+ import * as path from "node:path";
4
+ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
19
5
  import type { Subprocess } from "bun";
6
+ import { getAgentDir } from "../config";
7
+ import { getShellConfig, killProcessTree } from "../utils/shell";
8
+ import { getOrCreateSnapshot } from "../utils/shell-snapshot";
9
+ import { time } from "../utils/timings";
20
10
 
21
11
  const GATEWAY_DIR_NAME = "python-gateway";
22
12
  const GATEWAY_INFO_FILE = "gateway.json";
@@ -114,13 +104,13 @@ const CASE_INSENSITIVE_ENV = process.platform === "win32";
114
104
  const ACTIVE_ENV_ALLOWLIST = CASE_INSENSITIVE_ENV ? WINDOWS_ENV_ALLOWLIST : DEFAULT_ENV_ALLOWLIST;
115
105
 
116
106
  const NORMALIZED_ALLOWLIST = new Map(
117
- Array.from(ACTIVE_ENV_ALLOWLIST, (key) => [CASE_INSENSITIVE_ENV ? key.toUpperCase() : key, key] as const),
107
+ Array.from(ACTIVE_ENV_ALLOWLIST, key => [CASE_INSENSITIVE_ENV ? key.toUpperCase() : key, key] as const),
118
108
  );
119
109
  const NORMALIZED_DENYLIST = new Set(
120
- Array.from(DEFAULT_ENV_DENYLIST, (key) => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
110
+ Array.from(DEFAULT_ENV_DENYLIST, key => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
121
111
  );
122
112
  const NORMALIZED_ALLOW_PREFIXES = CASE_INSENSITIVE_ENV
123
- ? DEFAULT_ENV_ALLOW_PREFIXES.map((prefix) => prefix.toUpperCase())
113
+ ? DEFAULT_ENV_ALLOW_PREFIXES.map(prefix => prefix.toUpperCase())
124
114
  : DEFAULT_ENV_ALLOW_PREFIXES;
125
115
 
126
116
  function normalizeEnvKey(key: string): string {
@@ -129,7 +119,7 @@ function normalizeEnvKey(key: string): string {
129
119
 
130
120
  function resolvePathKey(env: Record<string, string | undefined>): string {
131
121
  if (!CASE_INSENSITIVE_ENV) return "PATH";
132
- const match = Object.keys(env).find((candidate) => candidate.toLowerCase() === "path");
122
+ const match = Object.keys(env).find(candidate => candidate.toLowerCase() === "path");
133
123
  return match ?? "PATH";
134
124
  }
135
125
 
@@ -166,7 +156,7 @@ function filterEnv(env: Record<string, string | undefined>): Record<string, stri
166
156
  filtered[canonicalKey] = value;
167
157
  continue;
168
158
  }
169
- if (NORMALIZED_ALLOW_PREFIXES.some((prefix) => normalizedKey.startsWith(prefix))) {
159
+ if (NORMALIZED_ALLOW_PREFIXES.some(prefix => normalizedKey.startsWith(prefix))) {
170
160
  filtered[key] = value;
171
161
  }
172
162
  }
@@ -175,7 +165,7 @@ function filterEnv(env: Record<string, string | undefined>): Record<string, stri
175
165
 
176
166
  async function resolveVenvPath(cwd: string): Promise<string | null> {
177
167
  if (process.env.VIRTUAL_ENV) return process.env.VIRTUAL_ENV;
178
- const candidates = [join(cwd, ".venv"), join(cwd, "venv")];
168
+ const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
179
169
  for (const candidate of candidates) {
180
170
  if (await Bun.file(candidate).exists()) {
181
171
  return candidate;
@@ -189,12 +179,12 @@ async function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
189
179
  const venvPath = env.VIRTUAL_ENV ?? (await resolveVenvPath(cwd));
190
180
  if (venvPath) {
191
181
  env.VIRTUAL_ENV = venvPath;
192
- const binDir = process.platform === "win32" ? join(venvPath, "Scripts") : join(venvPath, "bin");
193
- const pythonCandidate = join(binDir, process.platform === "win32" ? "python.exe" : "python");
182
+ const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
183
+ const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
194
184
  if (await Bun.file(pythonCandidate).exists()) {
195
185
  const pathKey = resolvePathKey(env);
196
186
  const currentPath = env[pathKey];
197
- env[pathKey] = currentPath ? `${binDir}${delimiter}${currentPath}` : binDir;
187
+ env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
198
188
  return { pythonPath: pythonCandidate, env, venvPath };
199
189
  }
200
190
  }
@@ -232,33 +222,29 @@ async function allocatePort(): Promise<number> {
232
222
  }
233
223
 
234
224
  function getGatewayDir(): string {
235
- return join(getAgentDir(), GATEWAY_DIR_NAME);
225
+ return path.join(getAgentDir(), GATEWAY_DIR_NAME);
236
226
  }
237
227
 
238
228
  function getGatewayInfoPath(): string {
239
- return join(getGatewayDir(), GATEWAY_INFO_FILE);
229
+ return path.join(getGatewayDir(), GATEWAY_INFO_FILE);
240
230
  }
241
231
 
242
232
  function getGatewayLockPath(): string {
243
- return join(getGatewayDir(), GATEWAY_LOCK_FILE);
233
+ return path.join(getGatewayDir(), GATEWAY_LOCK_FILE);
244
234
  }
245
235
 
246
- function writeLockInfo(lockPath: string, fd: number): void {
236
+ async function writeLockInfo(lockPath: string): Promise<void> {
247
237
  const payload: GatewayLockInfo = { pid: process.pid, startedAt: Date.now() };
248
238
  try {
249
- writeFileSync(fd, JSON.stringify(payload));
239
+ await Bun.write(lockPath, JSON.stringify(payload));
250
240
  } catch {
251
- try {
252
- writeFileSync(lockPath, JSON.stringify(payload));
253
- } catch {
254
- // Ignore lock write failures
255
- }
241
+ // Ignore lock write failures
256
242
  }
257
243
  }
258
244
 
259
- function readLockInfo(lockPath: string): GatewayLockInfo | null {
245
+ async function readLockInfo(lockPath: string): Promise<GatewayLockInfo | null> {
260
246
  try {
261
- const raw = readFileSync(lockPath, "utf-8");
247
+ const raw = await Bun.file(lockPath).text();
262
248
  const parsed = JSON.parse(raw) as Partial<GatewayLockInfo>;
263
249
  if (typeof parsed.pid === "number" && Number.isFinite(parsed.pid)) {
264
250
  return { pid: parsed.pid, startedAt: typeof parsed.startedAt === "number" ? parsed.startedAt : 0 };
@@ -269,36 +255,41 @@ function readLockInfo(lockPath: string): GatewayLockInfo | null {
269
255
  return null;
270
256
  }
271
257
 
272
- function ensureGatewayDir(): void {
258
+ async function ensureGatewayDir(): Promise<void> {
273
259
  const dir = getGatewayDir();
274
- if (!existsSync(dir)) {
275
- mkdirSync(dir, { recursive: true });
276
- }
260
+ await fs.promises.mkdir(dir, { recursive: true });
277
261
  }
278
262
 
279
263
  async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
280
- ensureGatewayDir();
264
+ await ensureGatewayDir();
281
265
  const lockPath = getGatewayLockPath();
282
266
  const start = Date.now();
283
267
  while (true) {
268
+ let fd: fs.promises.FileHandle | undefined;
284
269
  try {
285
- const fd = openSync(lockPath, "wx");
286
- const heartbeat = setInterval(() => {
287
- try {
288
- const now = new Date();
289
- utimesSync(lockPath, now, now);
290
- } catch {
291
- // Ignore heartbeat errors
270
+ fd = await fs.promises.open(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL);
271
+ let heartbeatRunning = true;
272
+ const heartbeat = (async () => {
273
+ while (heartbeatRunning) {
274
+ await Bun.sleep(GATEWAY_LOCK_HEARTBEAT_MS);
275
+ if (!heartbeatRunning) break;
276
+ try {
277
+ const now = new Date();
278
+ await fs.promises.utimes(lockPath, now, now);
279
+ } catch {
280
+ // Ignore heartbeat errors
281
+ }
292
282
  }
293
- }, GATEWAY_LOCK_HEARTBEAT_MS);
283
+ })();
294
284
  try {
295
- writeLockInfo(lockPath, fd);
285
+ await writeLockInfo(lockPath);
296
286
  return await handler();
297
287
  } finally {
298
- clearInterval(heartbeat);
288
+ heartbeatRunning = false;
289
+ void heartbeat.catch(() => {}); // Don't await - let it die naturally
299
290
  try {
300
- closeSync(fd);
301
- unlinkSync(lockPath);
291
+ await fd.close();
292
+ await fs.promises.unlink(lockPath);
302
293
  } catch {
303
294
  // Ignore lock cleanup errors
304
295
  }
@@ -308,15 +299,15 @@ async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
308
299
  if (error.code === "EEXIST") {
309
300
  let removedStale = false;
310
301
  try {
311
- const stat = statSync(lockPath);
312
- const lockInfo = readLockInfo(lockPath);
302
+ const lockStat = await fs.promises.stat(lockPath);
303
+ const lockInfo = await readLockInfo(lockPath);
313
304
  const lockPid = lockInfo?.pid;
314
- const lockAgeMs = lockInfo?.startedAt ? Date.now() - lockInfo.startedAt : Date.now() - stat.mtimeMs;
305
+ const lockAgeMs = lockInfo?.startedAt ? Date.now() - lockInfo.startedAt : Date.now() - lockStat.mtimeMs;
315
306
  const staleByTime = lockAgeMs > GATEWAY_LOCK_STALE_MS;
316
307
  const staleByPid = lockPid !== undefined && !isPidRunning(lockPid);
317
308
  const staleByMissingPid = lockPid === undefined && staleByTime;
318
309
  if (staleByPid || staleByMissingPid) {
319
- unlinkSync(lockPath);
310
+ await fs.promises.unlink(lockPath);
320
311
  removedStale = true;
321
312
  logger.warn("Removed stale shared gateway lock", { path: lockPath, pid: lockPid });
322
313
  }
@@ -336,14 +327,12 @@ async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
336
327
  }
337
328
  }
338
329
 
339
- function readGatewayInfo(): GatewayInfo | null {
330
+ async function readGatewayInfo(): Promise<GatewayInfo | null> {
340
331
  const infoPath = getGatewayInfoPath();
341
- if (!existsSync(infoPath)) return null;
342
-
343
332
  try {
344
- const content = readFileSync(infoPath, "utf-8");
333
+ const content = await Bun.file(infoPath).text();
345
334
  const parsed = JSON.parse(content) as Partial<GatewayInfo>;
346
- if (!parsed || typeof parsed !== "object") return null;
335
+
347
336
  if (typeof parsed.url !== "string" || typeof parsed.pid !== "number" || typeof parsed.startedAt !== "number") {
348
337
  return null;
349
338
  }
@@ -354,26 +343,25 @@ function readGatewayInfo(): GatewayInfo | null {
354
343
  pythonPath: typeof parsed.pythonPath === "string" ? parsed.pythonPath : undefined,
355
344
  venvPath: typeof parsed.venvPath === "string" || parsed.venvPath === null ? parsed.venvPath : undefined,
356
345
  };
357
- } catch {
346
+ } catch (err) {
347
+ if (isEnoent(err)) return null;
358
348
  return null;
359
349
  }
360
350
  }
361
351
 
362
- function writeGatewayInfo(info: GatewayInfo): void {
352
+ async function writeGatewayInfo(info: GatewayInfo): Promise<void> {
363
353
  const infoPath = getGatewayInfoPath();
364
354
  const tempPath = `${infoPath}.tmp`;
365
- writeFileSync(tempPath, JSON.stringify(info, null, 2));
366
- renameSync(tempPath, infoPath);
355
+ await Bun.write(tempPath, JSON.stringify(info, null, 2));
356
+ await fs.promises.rename(tempPath, infoPath);
367
357
  }
368
358
 
369
- function clearGatewayInfo(): void {
359
+ async function clearGatewayInfo(): Promise<void> {
370
360
  const infoPath = getGatewayInfoPath();
371
- if (existsSync(infoPath)) {
372
- try {
373
- unlinkSync(infoPath);
374
- } catch {
375
- // Ignore errors on cleanup
376
- }
361
+ try {
362
+ await fs.promises.unlink(infoPath);
363
+ } catch {
364
+ // Ignore errors on cleanup (file may not exist)
377
365
  }
378
366
  }
379
367
 
@@ -388,12 +376,9 @@ function isPidRunning(pid: number): boolean {
388
376
 
389
377
  async function isGatewayHealthy(url: string): Promise<boolean> {
390
378
  try {
391
- const controller = new AbortController();
392
- const timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
393
379
  const response = await fetch(`${url}/api/kernelspecs`, {
394
- signal: controller.signal,
380
+ signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
395
381
  });
396
- clearTimeout(timeout);
397
382
  return response.ok;
398
383
  } catch {
399
384
  return false;
@@ -497,9 +482,12 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
497
482
 
498
483
  try {
499
484
  return await withGatewayLock(async () => {
500
- const existingInfo = readGatewayInfo();
485
+ time("acquireSharedGateway:lockAcquired");
486
+ const existingInfo = await readGatewayInfo();
487
+ time("acquireSharedGateway:readInfo");
501
488
  if (existingInfo) {
502
489
  if (await isGatewayAlive(existingInfo)) {
490
+ time("acquireSharedGateway:isAlive");
503
491
  localGatewayUrl = existingInfo.url;
504
492
  isCoordinatorInitialized = true;
505
493
  logger.debug("Reusing global Python gateway", { url: existingInfo.url });
@@ -510,10 +498,11 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
510
498
  if (isPidRunning(existingInfo.pid)) {
511
499
  await killGateway(existingInfo.pid, "stale");
512
500
  }
513
- clearGatewayInfo();
501
+ await clearGatewayInfo();
514
502
  }
515
503
 
516
504
  const { url, pid, pythonPath, venvPath } = await startGatewayProcess(cwd);
505
+ time("acquireSharedGateway:startGateway");
517
506
  const info: GatewayInfo = {
518
507
  url,
519
508
  pid,
@@ -521,7 +510,7 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
521
510
  pythonPath,
522
511
  venvPath,
523
512
  };
524
- writeGatewayInfo(info);
513
+ await writeGatewayInfo(info);
525
514
  isCoordinatorInitialized = true;
526
515
  logger.debug("Started global Python gateway", { url, pid });
527
516
  return { url, isShared: true };
@@ -538,13 +527,13 @@ export async function releaseSharedGateway(): Promise<void> {
538
527
  if (!isCoordinatorInitialized) return;
539
528
  }
540
529
 
541
- export function getSharedGatewayUrl(): string | null {
530
+ export async function getSharedGatewayUrl(): Promise<string | null> {
542
531
  if (localGatewayUrl) return localGatewayUrl;
543
- return readGatewayInfo()?.url ?? null;
532
+ return (await readGatewayInfo())?.url ?? null;
544
533
  }
545
534
 
546
- export function isSharedGatewayActive(): boolean {
547
- return getGatewayStatus().active;
535
+ export async function isSharedGatewayActive(): Promise<boolean> {
536
+ return (await getGatewayStatus()).active;
548
537
  }
549
538
 
550
539
  export interface GatewayStatus {
@@ -556,8 +545,8 @@ export interface GatewayStatus {
556
545
  venvPath: string | null;
557
546
  }
558
547
 
559
- export function getGatewayStatus(): GatewayStatus {
560
- const info = readGatewayInfo();
548
+ export async function getGatewayStatus(): Promise<GatewayStatus> {
549
+ const info = await readGatewayInfo();
561
550
  if (!info) {
562
551
  return {
563
552
  active: false,
@@ -582,12 +571,12 @@ export function getGatewayStatus(): GatewayStatus {
582
571
  export async function shutdownSharedGateway(): Promise<void> {
583
572
  try {
584
573
  await withGatewayLock(async () => {
585
- const info = readGatewayInfo();
574
+ const info = await readGatewayInfo();
586
575
  if (!info) return;
587
576
  if (isPidRunning(info.pid)) {
588
577
  await killGateway(info.pid, "shutdown");
589
578
  }
590
- clearGatewayInfo();
579
+ await clearGatewayInfo();
591
580
  });
592
581
  } catch (err) {
593
582
  logger.warn("Failed to shutdown shared gateway", {
package/src/ipy/kernel.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { createServer } from "node:net";
2
- import { delimiter, join } from "node:path";
3
- import { getShellConfig, killProcessTree } from "@oh-my-pi/pi-coding-agent/utils/shell";
4
- import { getOrCreateSnapshot } from "@oh-my-pi/pi-coding-agent/utils/shell-snapshot";
5
- import { htmlToBasicMarkdown } from "@oh-my-pi/pi-coding-agent/web/scrapers/types";
2
+ import * as path from "node:path";
6
3
  import { logger } from "@oh-my-pi/pi-utils";
7
4
  import { $, type Subprocess } from "bun";
8
5
  import { nanoid } from "nanoid";
6
+ import { getShellConfig, killProcessTree } from "../utils/shell";
7
+ import { getOrCreateSnapshot } from "../utils/shell-snapshot";
8
+ import { time } from "../utils/timings";
9
+ import { htmlToBasicMarkdown } from "../web/scrapers/types";
9
10
  import { acquireSharedGateway, releaseSharedGateway } from "./gateway-coordinator";
10
11
  import { loadPythonModules } from "./modules";
11
12
  import { PYTHON_PRELUDE } from "./prelude";
@@ -131,13 +132,13 @@ const BASE_ENV_ALLOWLIST = CASE_INSENSITIVE_ENV
131
132
  ? new Set([...DEFAULT_ENV_ALLOWLIST, ...WINDOWS_ENV_ALLOWLIST])
132
133
  : DEFAULT_ENV_ALLOWLIST;
133
134
  const NORMALIZED_ALLOWLIST = new Set(
134
- Array.from(BASE_ENV_ALLOWLIST, (key) => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
135
+ Array.from(BASE_ENV_ALLOWLIST, key => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
135
136
  );
136
137
  const NORMALIZED_DENYLIST = new Set(
137
- Array.from(DEFAULT_ENV_DENYLIST, (key) => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
138
+ Array.from(DEFAULT_ENV_DENYLIST, key => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
138
139
  );
139
140
  const NORMALIZED_ALLOW_PREFIXES = CASE_INSENSITIVE_ENV
140
- ? DEFAULT_ENV_ALLOW_PREFIXES.map((prefix) => prefix.toUpperCase())
141
+ ? DEFAULT_ENV_ALLOW_PREFIXES.map(prefix => prefix.toUpperCase())
141
142
  : DEFAULT_ENV_ALLOW_PREFIXES;
142
143
 
143
144
  function normalizeEnvKey(key: string): string {
@@ -146,7 +147,7 @@ function normalizeEnvKey(key: string): string {
146
147
 
147
148
  function resolvePathKey(env: Record<string, string | undefined>): string {
148
149
  if (!CASE_INSENSITIVE_ENV) return "PATH";
149
- const match = Object.keys(env).find((candidate) => candidate.toLowerCase() === "path");
150
+ const match = Object.keys(env).find(candidate => candidate.toLowerCase() === "path");
150
151
  return match ?? "PATH";
151
152
  }
152
153
 
@@ -230,7 +231,7 @@ function filterEnv(env: Record<string, string | undefined>): Record<string, stri
230
231
  filtered[destKey] = value;
231
232
  continue;
232
233
  }
233
- if (NORMALIZED_ALLOW_PREFIXES.some((prefix) => normalizedKey.startsWith(prefix))) {
234
+ if (NORMALIZED_ALLOW_PREFIXES.some(prefix => normalizedKey.startsWith(prefix))) {
234
235
  filtered[key] = value;
235
236
  }
236
237
  }
@@ -239,7 +240,7 @@ function filterEnv(env: Record<string, string | undefined>): Record<string, stri
239
240
 
240
241
  async function resolveVenvPath(cwd: string): Promise<string | null> {
241
242
  if (process.env.VIRTUAL_ENV) return process.env.VIRTUAL_ENV;
242
- const candidates = [join(cwd, ".venv"), join(cwd, "venv")];
243
+ const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
243
244
  for (const candidate of candidates) {
244
245
  if (await Bun.file(candidate).exists()) {
245
246
  return candidate;
@@ -253,12 +254,12 @@ async function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
253
254
  const venvPath = env.VIRTUAL_ENV ?? (await resolveVenvPath(cwd));
254
255
  if (venvPath) {
255
256
  env.VIRTUAL_ENV = venvPath;
256
- const binDir = process.platform === "win32" ? join(venvPath, "Scripts") : join(venvPath, "bin");
257
- const pythonCandidate = join(binDir, process.platform === "win32" ? "python.exe" : "python");
257
+ const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
258
+ const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
258
259
  if (await Bun.file(pythonCandidate).exists()) {
259
260
  const pathKey = resolvePathKey(env);
260
261
  const currentPath = env[pathKey];
261
- env[pathKey] = currentPath ? `${binDir}${delimiter}${currentPath}` : binDir;
262
+ env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
262
263
  return { pythonPath: pythonCandidate, env };
263
264
  }
264
265
  }
@@ -309,13 +310,11 @@ async function checkExternalGatewayAvailability(config: ExternalGatewayConfig):
309
310
  }
310
311
 
311
312
  const controller = new AbortController();
312
- const timeout = setTimeout(() => controller.abort(), 5000);
313
313
 
314
314
  const response = await fetch(`${config.url}/api/kernelspecs`, {
315
315
  headers,
316
- signal: controller.signal,
316
+ signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]),
317
317
  });
318
- clearTimeout(timeout);
319
318
 
320
319
  if (response.ok) {
321
320
  return { ok: true };
@@ -505,6 +504,7 @@ export class PythonKernel {
505
504
 
506
505
  static async start(options: KernelStartOptions): Promise<PythonKernel> {
507
506
  const availability = await checkPythonKernelAvailability(options.cwd);
507
+ time("PythonKernel.start:availabilityCheck");
508
508
  if (!availability.ok) {
509
509
  throw new Error(availability.reason ?? "Python kernel unavailable");
510
510
  }
@@ -518,8 +518,11 @@ export class PythonKernel {
518
518
  if (options.useSharedGateway !== false) {
519
519
  try {
520
520
  const sharedResult = await acquireSharedGateway(options.cwd);
521
+ time("PythonKernel.start:acquireSharedGateway");
521
522
  if (sharedResult) {
522
- return PythonKernel.startWithSharedGateway(sharedResult.url, options.cwd, options.env);
523
+ const kernel = await PythonKernel.startWithSharedGateway(sharedResult.url, options.cwd, options.env);
524
+ time("PythonKernel.start:startWithSharedGateway");
525
+ return kernel;
523
526
  }
524
527
  } catch (err) {
525
528
  logger.warn("Failed to acquire shared gateway, falling back to local", {
@@ -582,6 +585,7 @@ export class PythonKernel {
582
585
  headers: { "Content-Type": "application/json" },
583
586
  body: JSON.stringify({ name: "python3" }),
584
587
  });
588
+ time("startWithSharedGateway:createKernel");
585
589
 
586
590
  if (!createResponse.ok) {
587
591
  await releaseSharedGateway();
@@ -595,13 +599,17 @@ export class PythonKernel {
595
599
 
596
600
  try {
597
601
  await kernel.connectWebSocket();
602
+ time("startWithSharedGateway:connectWS");
598
603
  await kernel.initializeKernelEnvironment(cwd, env);
604
+ time("startWithSharedGateway:initEnv");
599
605
  kernel.startHeartbeat();
600
606
  const preludeResult = await kernel.execute(PYTHON_PRELUDE, { silent: true, storeHistory: false });
607
+ time("startWithSharedGateway:prelude");
601
608
  if (preludeResult.cancelled || preludeResult.status === "error") {
602
609
  throw new Error("Failed to initialize Python kernel prelude");
603
610
  }
604
611
  await loadPythonModules(kernel, { cwd });
612
+ time("startWithSharedGateway:loadModules");
605
613
  return kernel;
606
614
  } catch (err: unknown) {
607
615
  await kernel.shutdown();
@@ -627,7 +635,7 @@ export class PythonKernel {
627
635
  OMP_SHELL_SNAPSHOT: snapshotPath ?? undefined,
628
636
  };
629
637
 
630
- const pythonPathParts = [options.cwd, kernelEnv.PYTHONPATH].filter(Boolean).join(delimiter);
638
+ const pythonPathParts = [options.cwd, kernelEnv.PYTHONPATH].filter(Boolean).join(path.delimiter);
631
639
  if (pythonPathParts) {
632
640
  kernelEnv.PYTHONPATH = pythonPathParts;
633
641
  }
@@ -754,7 +762,7 @@ export class PythonKernel {
754
762
  resolve();
755
763
  };
756
764
 
757
- ws.onerror = (event) => {
765
+ ws.onerror = event => {
758
766
  const error = new Error(`WebSocket error: ${event}`);
759
767
  if (!settled) {
760
768
  settled = true;
@@ -779,7 +787,7 @@ export class PythonKernel {
779
787
  this.abortPendingExecutions("WebSocket closed");
780
788
  };
781
789
 
782
- ws.onmessage = (event) => {
790
+ ws.onmessage = event => {
783
791
  let msg: JupyterMessage | null = null;
784
792
  if (event.data instanceof ArrayBuffer) {
785
793
  msg = deserializeWebSocketMessage(event.data);
@@ -950,7 +958,7 @@ export class PythonKernel {
950
958
  return promise;
951
959
  }
952
960
 
953
- this.#messageHandlers.set(msgId, async (response) => {
961
+ this.#messageHandlers.set(msgId, async response => {
954
962
  switch (response.header.msg_type) {
955
963
  case "execute_reply": {
956
964
  replyReceived = true;
@@ -1045,7 +1053,7 @@ export class PythonKernel {
1045
1053
  const result = await this.execute(PRELUDE_INTROSPECTION_SNIPPET, {
1046
1054
  silent: false,
1047
1055
  storeHistory: false,
1048
- onChunk: (text) => {
1056
+ onChunk: text => {
1049
1057
  output += text;
1050
1058
  },
1051
1059
  });
@@ -1064,14 +1072,11 @@ export class PythonKernel {
1064
1072
 
1065
1073
  async interrupt(): Promise<void> {
1066
1074
  try {
1067
- const controller = new AbortController();
1068
- const timeout = setTimeout(() => controller.abort(), 2000);
1069
1075
  await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}/interrupt`, {
1070
1076
  method: "POST",
1071
1077
  headers: this.#authHeaders(),
1072
- signal: controller.signal,
1078
+ signal: AbortSignal.timeout(2000),
1073
1079
  });
1074
- clearTimeout(timeout);
1075
1080
  } catch (err: unknown) {
1076
1081
  logger.warn("Failed to interrupt kernel via API", { error: err instanceof Error ? err.message : String(err) });
1077
1082
  }
@@ -1138,13 +1143,10 @@ export class PythonKernel {
1138
1143
  async ping(timeoutMs: number = HEARTBEAT_TIMEOUT_MS): Promise<boolean> {
1139
1144
  if (!this.isAlive()) return false;
1140
1145
  try {
1141
- const controller = new AbortController();
1142
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1143
1146
  const response = await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}`, {
1144
- signal: controller.signal,
1147
+ signal: AbortSignal.timeout(timeoutMs),
1145
1148
  headers: this.#authHeaders(),
1146
1149
  });
1147
- clearTimeout(timeout);
1148
1150
  if (response.ok) {
1149
1151
  this.#heartbeatFailures = 0;
1150
1152
  return true;