@oh-my-pi/pi-coding-agent 8.0.20 → 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.
- package/CHANGELOG.md +125 -0
- package/docs/session.md +111 -46
- package/examples/custom-tools/hello/index.ts +1 -1
- package/examples/custom-tools/todo/index.ts +3 -4
- package/examples/extensions/api-demo.ts +0 -1
- package/examples/extensions/chalk-logger.ts +2 -3
- package/examples/extensions/hello.ts +0 -1
- package/examples/extensions/pirate.ts +0 -1
- package/examples/extensions/plan-mode.ts +15 -16
- package/examples/extensions/todo.ts +3 -4
- package/examples/extensions/tools.ts +1 -2
- package/examples/extensions/with-deps/index.ts +0 -1
- package/examples/hooks/auto-commit-on-exit.ts +1 -2
- package/examples/hooks/confirm-destructive.ts +0 -1
- package/examples/hooks/custom-compaction.ts +1 -2
- package/examples/hooks/dirty-repo-guard.ts +0 -1
- package/examples/hooks/file-trigger.ts +3 -4
- package/examples/hooks/git-checkpoint.ts +0 -1
- package/examples/hooks/handoff.ts +3 -4
- package/examples/hooks/permission-gate.ts +1 -2
- package/examples/hooks/protected-paths.ts +1 -2
- package/examples/hooks/qna.ts +2 -3
- package/examples/hooks/snake.ts +4 -5
- package/examples/hooks/status-line.ts +0 -1
- package/examples/sdk/01-minimal.ts +2 -3
- package/examples/sdk/02-custom-model.ts +2 -3
- package/examples/sdk/03-custom-prompt.ts +3 -4
- package/examples/sdk/04-skills.ts +2 -3
- package/examples/sdk/06-extensions.ts +1 -2
- package/examples/sdk/06-hooks.ts +6 -7
- package/examples/sdk/07-context-files.ts +0 -1
- package/examples/sdk/08-prompt-templates.ts +0 -1
- package/examples/sdk/08-slash-commands.ts +0 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +0 -1
- package/examples/sdk/10-settings.ts +0 -1
- package/examples/sdk/11-sessions.ts +0 -1
- package/package.json +54 -23
- package/scripts/format-prompts.ts +0 -1
- package/src/capability/context-file.ts +3 -4
- package/src/capability/extension-module.ts +3 -4
- package/src/capability/extension.ts +3 -4
- package/src/capability/fs.ts +20 -21
- package/src/capability/hook.ts +3 -4
- package/src/capability/index.ts +15 -16
- package/src/capability/instruction.ts +3 -4
- package/src/capability/mcp.ts +3 -4
- package/src/capability/prompt.ts +3 -4
- package/src/capability/rule.ts +3 -4
- package/src/capability/settings.ts +2 -3
- package/src/capability/skill.ts +3 -4
- package/src/capability/slash-command.ts +3 -4
- package/src/capability/ssh.ts +3 -4
- package/src/capability/system-prompt.ts +3 -4
- package/src/capability/tool.ts +3 -4
- package/src/cli/args.ts +5 -6
- package/src/cli/config-cli.ts +6 -7
- package/src/cli/file-processor.ts +19 -17
- package/src/cli/jupyter-cli.ts +105 -0
- package/src/cli/list-models.ts +10 -11
- package/src/cli/plugin-cli.ts +20 -25
- package/src/cli/session-picker.ts +2 -3
- package/src/cli/setup-cli.ts +2 -3
- package/src/cli/stats-cli.ts +2 -3
- package/src/cli/update-cli.ts +25 -22
- package/src/commit/agentic/agent.ts +307 -0
- package/src/commit/agentic/fallback.ts +96 -0
- package/src/commit/agentic/index.ts +351 -0
- package/src/commit/agentic/prompts/analyze-file.md +22 -0
- package/src/commit/agentic/prompts/session-user.md +26 -0
- package/src/commit/agentic/prompts/split-confirm.md +1 -0
- package/src/commit/agentic/prompts/system.md +40 -0
- package/src/commit/agentic/state.ts +69 -0
- package/src/commit/agentic/tools/analyze-file.ts +131 -0
- package/src/commit/agentic/tools/git-file-diff.ts +194 -0
- package/src/commit/agentic/tools/git-hunk.ts +50 -0
- package/src/commit/agentic/tools/git-overview.ts +84 -0
- package/src/commit/agentic/tools/index.ts +56 -0
- package/src/commit/agentic/tools/propose-changelog.ts +128 -0
- package/src/commit/agentic/tools/propose-commit.ts +154 -0
- package/src/commit/agentic/tools/recent-commits.ts +81 -0
- package/src/commit/agentic/tools/split-commit.ts +280 -0
- package/src/commit/agentic/topo-sort.ts +44 -0
- package/src/commit/agentic/trivial.ts +51 -0
- package/src/commit/agentic/validation.ts +200 -0
- package/src/commit/analysis/conventional.ts +165 -0
- package/src/commit/analysis/index.ts +4 -0
- package/src/commit/analysis/scope.ts +242 -0
- package/src/commit/analysis/summary.ts +112 -0
- package/src/commit/analysis/validation.ts +66 -0
- package/src/commit/changelog/detect.ts +36 -0
- package/src/commit/changelog/generate.ts +110 -0
- package/src/commit/changelog/index.ts +233 -0
- package/src/commit/changelog/parse.ts +44 -0
- package/src/commit/cli.ts +93 -0
- package/src/commit/git/diff.ts +148 -0
- package/src/commit/git/errors.ts +11 -0
- package/src/commit/git/index.ts +212 -0
- package/src/commit/git/operations.ts +53 -0
- package/src/commit/index.ts +5 -0
- package/src/commit/map-reduce/index.ts +63 -0
- package/src/commit/map-reduce/map-phase.ts +178 -0
- package/src/commit/map-reduce/reduce-phase.ts +145 -0
- package/src/commit/map-reduce/utils.ts +9 -0
- package/src/commit/message.ts +11 -0
- package/src/commit/model-selection.ts +80 -0
- package/src/commit/pipeline.ts +240 -0
- package/src/commit/prompts/analysis-system.md +155 -0
- package/src/commit/prompts/analysis-user.md +41 -0
- package/src/commit/prompts/changelog-system.md +56 -0
- package/src/commit/prompts/changelog-user.md +19 -0
- package/src/commit/prompts/file-observer-system.md +26 -0
- package/src/commit/prompts/file-observer-user.md +9 -0
- package/src/commit/prompts/reduce-system.md +60 -0
- package/src/commit/prompts/reduce-user.md +17 -0
- package/src/commit/prompts/summary-retry.md +4 -0
- package/src/commit/prompts/summary-system.md +52 -0
- package/src/commit/prompts/summary-user.md +13 -0
- package/src/commit/prompts/types-description.md +2 -0
- package/src/commit/types.ts +109 -0
- package/src/commit/utils/exclusions.ts +42 -0
- package/src/config/file-lock.ts +121 -0
- package/src/config/keybindings.ts +6 -8
- package/src/config/model-registry.ts +65 -38
- package/src/config/model-resolver.ts +18 -19
- package/src/config/prompt-templates.ts +11 -11
- package/src/config/settings-manager.ts +141 -50
- package/src/config.ts +64 -66
- package/src/cursor.ts +11 -9
- package/src/discovery/agents-md.ts +11 -12
- package/src/discovery/builtin.ts +68 -73
- package/src/discovery/claude.ts +41 -42
- package/src/discovery/cline.ts +11 -12
- package/src/discovery/codex.ts +52 -53
- package/src/discovery/cursor.ts +9 -10
- package/src/discovery/gemini.ts +17 -22
- package/src/discovery/github.ts +13 -14
- package/src/discovery/helpers.ts +35 -34
- package/src/discovery/index.ts +22 -24
- package/src/discovery/mcp-json.ts +8 -9
- package/src/discovery/ssh.ts +8 -9
- package/src/discovery/vscode.ts +4 -5
- package/src/discovery/windsurf.ts +6 -7
- package/src/exa/company.ts +1 -2
- package/src/exa/index.ts +2 -3
- package/src/exa/linkedin.ts +1 -2
- package/src/exa/mcp-client.ts +14 -16
- package/src/exa/render.ts +10 -11
- package/src/exa/researcher.ts +1 -2
- package/src/exa/search.ts +1 -2
- package/src/exa/types.ts +0 -1
- package/src/exa/websets.ts +1 -2
- package/src/exec/bash-executor.ts +3 -4
- package/src/exec/exec.ts +0 -1
- package/src/export/custom-share.ts +5 -6
- package/src/export/html/index.ts +24 -21
- package/src/export/ttsr.ts +2 -3
- package/src/extensibility/custom-commands/bundled/review/index.ts +7 -8
- package/src/extensibility/custom-commands/loader.ts +18 -15
- package/src/extensibility/custom-commands/types.ts +2 -3
- package/src/extensibility/custom-tools/loader.ts +11 -12
- package/src/extensibility/custom-tools/types.ts +7 -8
- package/src/extensibility/custom-tools/wrapper.ts +2 -3
- package/src/extensibility/extensions/loader.ts +76 -54
- package/src/extensibility/extensions/runner.ts +11 -12
- package/src/extensibility/extensions/types.ts +20 -27
- package/src/extensibility/extensions/wrapper.ts +3 -4
- package/src/extensibility/hooks/index.ts +1 -1
- package/src/extensibility/hooks/loader.ts +9 -10
- package/src/extensibility/hooks/runner.ts +7 -8
- package/src/extensibility/hooks/tool-wrapper.ts +0 -1
- package/src/extensibility/hooks/types.ts +11 -18
- package/src/extensibility/plugins/doctor.ts +3 -3
- package/src/extensibility/plugins/installer.ts +27 -27
- package/src/extensibility/plugins/loader.ts +59 -56
- package/src/extensibility/plugins/manager.ts +211 -171
- package/src/extensibility/plugins/parser.ts +1 -1
- package/src/extensibility/plugins/paths.ts +8 -8
- package/src/extensibility/skills.ts +63 -60
- package/src/extensibility/slash-commands.ts +10 -10
- package/src/index.ts +54 -54
- package/src/internal-urls/agent-protocol.ts +21 -11
- package/src/internal-urls/artifact-protocol.ts +17 -13
- package/src/internal-urls/router.ts +1 -2
- package/src/internal-urls/rule-protocol.ts +3 -4
- package/src/internal-urls/skill-protocol.ts +3 -4
- package/src/ipy/executor.ts +109 -9
- package/src/ipy/gateway-coordinator.ts +79 -90
- package/src/ipy/kernel.ts +32 -30
- package/src/ipy/modules.ts +13 -13
- package/src/lsp/client.ts +21 -10
- package/src/lsp/clients/biome-client.ts +1 -2
- package/src/lsp/clients/index.ts +3 -3
- package/src/lsp/clients/lsp-linter-client.ts +4 -5
- package/src/lsp/config.ts +15 -15
- package/src/lsp/edits.ts +4 -5
- package/src/lsp/index.ts +43 -44
- package/src/lsp/lspmux.ts +8 -8
- package/src/lsp/render.ts +99 -61
- package/src/lsp/utils.ts +3 -3
- package/src/main.ts +71 -37
- package/src/mcp/client.ts +2 -3
- package/src/mcp/config.ts +5 -6
- package/src/mcp/json-rpc.ts +0 -1
- package/src/mcp/loader.ts +6 -7
- package/src/mcp/manager.ts +17 -18
- package/src/mcp/tool-bridge.ts +4 -9
- package/src/mcp/tool-cache.ts +2 -3
- package/src/mcp/transports/http.ts +2 -4
- package/src/mcp/transports/stdio.ts +1 -2
- package/src/migrations.ts +63 -52
- package/src/modes/components/armin.ts +4 -5
- package/src/modes/components/assistant-message.ts +33 -5
- package/src/modes/components/bash-execution.ts +7 -8
- package/src/modes/components/bordered-loader.ts +3 -3
- package/src/modes/components/branch-summary-message.ts +3 -3
- package/src/modes/components/compaction-summary-message.ts +3 -3
- package/src/modes/components/countdown-timer.ts +0 -1
- package/src/modes/components/custom-message.ts +5 -5
- package/src/modes/components/diff.ts +1 -1
- package/src/modes/components/dynamic-border.ts +2 -2
- package/src/modes/components/extensions/extension-dashboard.ts +6 -7
- package/src/modes/components/extensions/extension-list.ts +2 -3
- package/src/modes/components/extensions/inspector-panel.ts +3 -4
- package/src/modes/components/extensions/state-manager.ts +25 -26
- package/src/modes/components/extensions/types.ts +1 -2
- package/src/modes/components/footer.ts +47 -43
- package/src/modes/components/history-search.ts +2 -2
- package/src/modes/components/hook-editor.ts +3 -4
- package/src/modes/components/hook-input.ts +2 -3
- package/src/modes/components/hook-message.ts +5 -5
- package/src/modes/components/hook-selector.ts +2 -3
- package/src/modes/components/keybinding-hints.ts +2 -3
- package/src/modes/components/login-dialog.ts +2 -2
- package/src/modes/components/model-selector.ts +12 -12
- package/src/modes/components/oauth-selector.ts +2 -2
- package/src/modes/components/plugin-settings.ts +20 -20
- package/src/modes/components/python-execution.ts +7 -8
- package/src/modes/components/queue-mode-selector.ts +3 -3
- package/src/modes/components/read-tool-group.ts +2 -2
- package/src/modes/components/session-selector.ts +4 -4
- package/src/modes/components/settings-defs.ts +77 -69
- package/src/modes/components/settings-selector.ts +16 -16
- package/src/modes/components/show-images-selector.ts +2 -2
- package/src/modes/components/status-line/segments.ts +4 -4
- package/src/modes/components/status-line/separators.ts +1 -1
- package/src/modes/components/status-line/types.ts +2 -2
- package/src/modes/components/status-line-segment-editor.ts +7 -8
- package/src/modes/components/status-line.ts +12 -12
- package/src/modes/components/theme-selector.ts +8 -7
- package/src/modes/components/thinking-selector.ts +4 -4
- package/src/modes/components/todo-display.ts +2 -2
- package/src/modes/components/todo-reminder.ts +4 -4
- package/src/modes/components/tool-execution.ts +16 -19
- package/src/modes/components/tree-selector.ts +12 -12
- package/src/modes/components/ttsr-notification.ts +5 -5
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/components/visual-truncate.ts +0 -1
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/command-controller.ts +46 -47
- package/src/modes/controllers/event-controller.ts +16 -20
- package/src/modes/controllers/extension-ui-controller.ts +40 -46
- package/src/modes/controllers/input-controller.ts +17 -18
- package/src/modes/controllers/selector-controller.ts +103 -91
- package/src/modes/index.ts +3 -3
- package/src/modes/interactive-mode.ts +31 -31
- package/src/modes/print-mode.ts +12 -13
- package/src/modes/rpc/rpc-client.ts +7 -8
- package/src/modes/rpc/rpc-mode.ts +24 -28
- package/src/modes/rpc/rpc-types.ts +3 -4
- package/src/modes/theme/mermaid-cache.ts +89 -0
- package/src/modes/theme/theme.ts +130 -53
- package/src/modes/types.ts +10 -10
- package/src/modes/utils/ui-helpers.ts +17 -17
- package/src/patch/applicator.ts +18 -19
- package/src/patch/diff.ts +1 -2
- package/src/patch/fuzzy.ts +1 -2
- package/src/patch/index.ts +11 -18
- package/src/patch/normalize.ts +4 -4
- package/src/patch/normative.ts +1 -2
- package/src/patch/parser.ts +8 -9
- package/src/patch/shared.ts +43 -16
- package/src/prompts/tools/task.md +2 -0
- package/src/sdk.ts +100 -65
- package/src/session/agent-session.ts +84 -85
- package/src/session/agent-storage.ts +43 -39
- package/src/session/artifacts.ts +32 -10
- package/src/session/auth-storage.ts +50 -39
- package/src/session/compaction/branch-summarization.ts +7 -10
- package/src/session/compaction/compaction.ts +8 -19
- package/src/session/compaction/utils.ts +6 -9
- package/src/session/history-storage.ts +10 -10
- package/src/session/messages.ts +4 -5
- package/src/session/session-manager.ts +76 -65
- package/src/session/session-storage.ts +57 -69
- package/src/session/storage-migration.ts +14 -56
- package/src/session/streaming-output.ts +2 -2
- package/src/ssh/connection-manager.ts +43 -50
- package/src/ssh/ssh-executor.ts +2 -2
- package/src/ssh/sshfs-mount.ts +11 -18
- package/src/system-prompt.ts +28 -35
- package/src/task/agents.ts +45 -30
- package/src/task/commands.ts +6 -7
- package/src/task/discovery.ts +39 -76
- package/src/task/executor.ts +14 -15
- package/src/task/index.ts +40 -34
- package/src/task/output-manager.ts +93 -0
- package/src/task/parallel.ts +0 -1
- package/src/task/render.ts +24 -30
- package/src/task/subprocess-tool-registry.ts +1 -2
- package/src/task/worker-protocol.ts +3 -3
- package/src/task/worker.ts +33 -39
- package/src/task/worktree.ts +19 -19
- package/src/tools/ask.ts +41 -20
- package/src/tools/bash-interceptor.ts +1 -5
- package/src/tools/bash.ts +91 -97
- package/src/tools/calculator.ts +49 -47
- package/src/tools/complete.ts +4 -5
- package/src/tools/context.ts +2 -2
- package/src/tools/fetch.ts +84 -124
- package/src/tools/find.ts +94 -98
- package/src/tools/gemini-image.ts +14 -14
- package/src/tools/grep.ts +100 -116
- package/src/tools/index.ts +80 -55
- package/src/tools/list-limit.ts +1 -1
- package/src/tools/ls.ts +44 -70
- package/src/tools/notebook.ts +51 -67
- package/src/tools/output-meta.ts +3 -4
- package/src/tools/output-utils.ts +2 -2
- package/src/tools/path-utils.ts +5 -5
- package/src/tools/python.ts +104 -217
- package/src/tools/read.ts +92 -33
- package/src/tools/render-utils.ts +8 -23
- package/src/tools/renderers.ts +6 -7
- package/src/tools/review.ts +8 -11
- package/src/tools/ssh.ts +69 -49
- package/src/tools/todo-write.ts +37 -25
- package/src/tools/tool-errors.ts +3 -3
- package/src/tools/tool-result.ts +3 -8
- package/src/tools/write.ts +99 -75
- package/src/tui/code-cell.ts +109 -0
- package/src/tui/file-list.ts +47 -0
- package/src/tui/index.ts +11 -0
- package/src/tui/output-block.ts +72 -0
- package/src/tui/status-line.ts +39 -0
- package/src/tui/tree-list.ts +55 -0
- package/src/tui/types.ts +16 -0
- package/src/tui/utils.ts +48 -0
- package/src/utils/changelog.ts +9 -10
- package/src/utils/clipboard.ts +11 -11
- package/src/utils/file-mentions.ts +4 -10
- package/src/utils/frontmatter.ts +6 -3
- package/src/utils/fuzzy.ts +2 -2
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +1 -1
- package/src/utils/mime.ts +2 -2
- package/src/utils/shell-snapshot.ts +11 -13
- package/src/utils/shell.ts +4 -5
- package/src/utils/title-generator.ts +8 -9
- package/src/utils/tools-manager.ts +23 -23
- package/src/vendor/photon/index.js +1099 -1059
- package/src/vendor/photon/photon_rs_bg.wasm +0 -0
- package/src/web/scrapers/artifacthub.ts +1 -1
- package/src/web/scrapers/arxiv.ts +2 -2
- package/src/web/scrapers/bluesky.ts +2 -2
- package/src/web/scrapers/cheatsh.ts +1 -1
- package/src/web/scrapers/chocolatey.ts +2 -2
- package/src/web/scrapers/choosealicense.ts +5 -5
- package/src/web/scrapers/cisa-kev.ts +1 -1
- package/src/web/scrapers/crossref.ts +2 -2
- package/src/web/scrapers/devto.ts +3 -3
- package/src/web/scrapers/discogs.ts +3 -4
- package/src/web/scrapers/discourse.ts +1 -1
- package/src/web/scrapers/dockerhub.ts +1 -1
- package/src/web/scrapers/fdroid.ts +2 -2
- package/src/web/scrapers/firefox-addons.ts +3 -3
- package/src/web/scrapers/flathub.ts +1 -1
- package/src/web/scrapers/github.ts +3 -3
- package/src/web/scrapers/gitlab.ts +4 -4
- package/src/web/scrapers/hackernews.ts +2 -2
- package/src/web/scrapers/huggingface.ts +1 -1
- package/src/web/scrapers/iacr.ts +2 -2
- package/src/web/scrapers/index.ts +0 -1
- package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
- package/src/web/scrapers/lemmy.ts +2 -2
- package/src/web/scrapers/maven.ts +2 -2
- package/src/web/scrapers/mdn.ts +2 -4
- package/src/web/scrapers/metacpan.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/npm.ts +1 -1
- package/src/web/scrapers/nuget.ts +2 -2
- package/src/web/scrapers/nvd.ts +3 -3
- package/src/web/scrapers/ollama.ts +7 -9
- package/src/web/scrapers/opencorporates.ts +2 -2
- package/src/web/scrapers/openlibrary.ts +6 -6
- package/src/web/scrapers/orcid.ts +0 -1
- package/src/web/scrapers/osv.ts +2 -2
- package/src/web/scrapers/packagist.ts +1 -1
- package/src/web/scrapers/pubmed.ts +1 -2
- package/src/web/scrapers/rawg.ts +2 -2
- package/src/web/scrapers/readthedocs.ts +1 -2
- package/src/web/scrapers/repology.ts +2 -2
- package/src/web/scrapers/rfc.ts +1 -1
- package/src/web/scrapers/searchcode.ts +2 -2
- package/src/web/scrapers/semantic-scholar.ts +1 -1
- package/src/web/scrapers/snapcraft.ts +2 -2
- package/src/web/scrapers/sourcegraph.ts +1 -1
- package/src/web/scrapers/spdx.ts +3 -3
- package/src/web/scrapers/spotify.ts +0 -1
- package/src/web/scrapers/twitter.ts +1 -1
- package/src/web/scrapers/types.ts +1 -2
- package/src/web/scrapers/utils.ts +5 -5
- package/src/web/scrapers/wikidata.ts +3 -3
- package/src/web/scrapers/youtube.ts +9 -14
- package/src/web/search/auth.ts +5 -10
- package/src/web/search/index.ts +11 -21
- package/src/web/search/providers/anthropic.ts +3 -9
- package/src/web/search/providers/exa.ts +6 -10
- package/src/web/search/providers/perplexity.ts +5 -5
- package/src/web/search/render.ts +129 -175
- package/tsconfig.json +0 -42
package/src/ipy/executor.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
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";
|
|
1
5
|
import {
|
|
2
6
|
checkPythonKernelAvailability,
|
|
3
7
|
type KernelDisplayOutput,
|
|
@@ -5,9 +9,12 @@ import {
|
|
|
5
9
|
type KernelExecuteResult,
|
|
6
10
|
type PreludeHelper,
|
|
7
11
|
PythonKernel,
|
|
8
|
-
} from "
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
} from "./kernel";
|
|
13
|
+
|
|
14
|
+
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
15
|
+
const MAX_KERNEL_SESSIONS = 4;
|
|
16
|
+
const CLEANUP_INTERVAL_MS = 30 * 1000; // 30 seconds
|
|
17
|
+
|
|
11
18
|
export type PythonKernelMode = "session" | "per-call";
|
|
12
19
|
|
|
13
20
|
export interface PythonExecutorOptions {
|
|
@@ -77,10 +84,60 @@ interface KernelSession {
|
|
|
77
84
|
|
|
78
85
|
const kernelSessions = new Map<string, KernelSession>();
|
|
79
86
|
let cachedPreludeDocs: PreludeHelper[] | null = null;
|
|
87
|
+
let cleanupTimer: NodeJS.Timeout | null = null;
|
|
88
|
+
|
|
89
|
+
function startCleanupTimer(): void {
|
|
90
|
+
if (cleanupTimer) return;
|
|
91
|
+
cleanupTimer = setInterval(() => {
|
|
92
|
+
void cleanupIdleSessions();
|
|
93
|
+
}, CLEANUP_INTERVAL_MS);
|
|
94
|
+
cleanupTimer.unref();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopCleanupTimer(): void {
|
|
98
|
+
if (cleanupTimer) {
|
|
99
|
+
clearInterval(cleanupTimer);
|
|
100
|
+
cleanupTimer = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function cleanupIdleSessions(): Promise<void> {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const toDispose: KernelSession[] = [];
|
|
107
|
+
|
|
108
|
+
for (const session of kernelSessions.values()) {
|
|
109
|
+
if (session.dead || now - session.lastUsedAt > IDLE_TIMEOUT_MS) {
|
|
110
|
+
toDispose.push(session);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (toDispose.length > 0) {
|
|
115
|
+
logger.debug("Cleaning up idle kernel sessions", { count: toDispose.length });
|
|
116
|
+
await Promise.allSettled(toDispose.map(session => disposeKernelSession(session)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (kernelSessions.size === 0) {
|
|
120
|
+
stopCleanupTimer();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function evictOldestSession(): Promise<void> {
|
|
125
|
+
let oldest: KernelSession | null = null;
|
|
126
|
+
for (const session of kernelSessions.values()) {
|
|
127
|
+
if (!oldest || session.lastUsedAt < oldest.lastUsedAt) {
|
|
128
|
+
oldest = session;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (oldest) {
|
|
132
|
+
logger.debug("Evicting oldest kernel session", { id: oldest.id });
|
|
133
|
+
await disposeKernelSession(oldest);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
80
136
|
|
|
81
137
|
export async function disposeAllKernelSessions(): Promise<void> {
|
|
138
|
+
stopCleanupTimer();
|
|
82
139
|
const sessions = Array.from(kernelSessions.values());
|
|
83
|
-
await Promise.allSettled(sessions.map(
|
|
140
|
+
await Promise.allSettled(sessions.map(session => disposeKernelSession(session)));
|
|
84
141
|
}
|
|
85
142
|
|
|
86
143
|
async function ensureKernelAvailable(cwd: string): Promise<void> {
|
|
@@ -98,6 +155,7 @@ export async function warmPythonEnvironment(
|
|
|
98
155
|
): Promise<{ ok: boolean; reason?: string; docs: PreludeHelper[] }> {
|
|
99
156
|
try {
|
|
100
157
|
await ensureKernelAvailable(cwd);
|
|
158
|
+
time("warmPython:ensureKernelAvailable");
|
|
101
159
|
} catch (err: unknown) {
|
|
102
160
|
const reason = err instanceof Error ? err.message : String(err);
|
|
103
161
|
cachedPreludeDocs = [];
|
|
@@ -111,10 +169,11 @@ export async function warmPythonEnvironment(
|
|
|
111
169
|
const docs = await withKernelSession(
|
|
112
170
|
resolvedSessionId,
|
|
113
171
|
cwd,
|
|
114
|
-
async
|
|
172
|
+
async kernel => kernel.introspectPrelude(),
|
|
115
173
|
useSharedGateway,
|
|
116
174
|
sessionFile,
|
|
117
175
|
);
|
|
176
|
+
time("warmPython:withKernelSession");
|
|
118
177
|
cachedPreludeDocs = docs;
|
|
119
178
|
return { ok: true, docs };
|
|
120
179
|
} catch (err: unknown) {
|
|
@@ -132,12 +191,36 @@ export function resetPreludeDocsCache(): void {
|
|
|
132
191
|
cachedPreludeDocs = null;
|
|
133
192
|
}
|
|
134
193
|
|
|
194
|
+
function isResourceExhaustionError(error: unknown): boolean {
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
return (
|
|
197
|
+
message.includes("Too many open files") ||
|
|
198
|
+
message.includes("EMFILE") ||
|
|
199
|
+
message.includes("ENFILE") ||
|
|
200
|
+
message.includes("resource temporarily unavailable")
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function recoverFromResourceExhaustion(): Promise<void> {
|
|
205
|
+
logger.warn("Resource exhaustion detected, recovering by restarting shared gateway");
|
|
206
|
+
stopCleanupTimer();
|
|
207
|
+
const sessions = Array.from(kernelSessions.values());
|
|
208
|
+
for (const session of sessions) {
|
|
209
|
+
if (session.heartbeatTimer) {
|
|
210
|
+
clearInterval(session.heartbeatTimer);
|
|
211
|
+
}
|
|
212
|
+
kernelSessions.delete(session.id);
|
|
213
|
+
}
|
|
214
|
+
await shutdownSharedGateway();
|
|
215
|
+
}
|
|
216
|
+
|
|
135
217
|
async function createKernelSession(
|
|
136
218
|
sessionId: string,
|
|
137
219
|
cwd: string,
|
|
138
220
|
useSharedGateway?: boolean,
|
|
139
221
|
sessionFile?: string,
|
|
140
222
|
artifactsDir?: string,
|
|
223
|
+
isRetry?: boolean,
|
|
141
224
|
): Promise<KernelSession> {
|
|
142
225
|
const env: Record<string, string> | undefined =
|
|
143
226
|
sessionFile || artifactsDir
|
|
@@ -146,7 +229,19 @@ async function createKernelSession(
|
|
|
146
229
|
...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
|
|
147
230
|
}
|
|
148
231
|
: undefined;
|
|
149
|
-
|
|
232
|
+
|
|
233
|
+
let kernel: PythonKernel;
|
|
234
|
+
try {
|
|
235
|
+
kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
|
|
236
|
+
time("createKernelSession:PythonKernel.start");
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (!isRetry && isResourceExhaustionError(err)) {
|
|
239
|
+
await recoverFromResourceExhaustion();
|
|
240
|
+
return createKernelSession(sessionId, cwd, useSharedGateway, sessionFile, artifactsDir, true);
|
|
241
|
+
}
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
|
|
150
245
|
const session: KernelSession = {
|
|
151
246
|
id: sessionId,
|
|
152
247
|
kernel,
|
|
@@ -218,8 +313,13 @@ async function withKernelSession<T>(
|
|
|
218
313
|
): Promise<T> {
|
|
219
314
|
let session = kernelSessions.get(sessionId);
|
|
220
315
|
if (!session) {
|
|
316
|
+
// Evict oldest session if at capacity
|
|
317
|
+
if (kernelSessions.size >= MAX_KERNEL_SESSIONS) {
|
|
318
|
+
await evictOldestSession();
|
|
319
|
+
}
|
|
221
320
|
session = await createKernelSession(sessionId, cwd, useSharedGateway, sessionFile, artifactsDir);
|
|
222
321
|
kernelSessions.set(sessionId, session);
|
|
322
|
+
startCleanupTimer();
|
|
223
323
|
}
|
|
224
324
|
|
|
225
325
|
const run = async (): Promise<T> => {
|
|
@@ -266,8 +366,8 @@ async function executeWithKernel(
|
|
|
266
366
|
const result = await kernel.execute(code, {
|
|
267
367
|
signal: options?.signal,
|
|
268
368
|
timeoutMs: options?.timeoutMs,
|
|
269
|
-
onChunk:
|
|
270
|
-
onDisplay:
|
|
369
|
+
onChunk: text => sink.push(text),
|
|
370
|
+
onDisplay: output => void displayOutputs.push(output),
|
|
271
371
|
});
|
|
272
372
|
|
|
273
373
|
if (result.cancelled) {
|
|
@@ -351,7 +451,7 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
351
451
|
return await withKernelSession(
|
|
352
452
|
sessionId,
|
|
353
453
|
cwd,
|
|
354
|
-
async
|
|
454
|
+
async kernel => executeWithKernel(kernel, code, options),
|
|
355
455
|
useSharedGateway,
|
|
356
456
|
sessionFile,
|
|
357
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
|
|
15
|
-
import {
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
236
|
+
async function writeLockInfo(lockPath: string): Promise<void> {
|
|
247
237
|
const payload: GatewayLockInfo = { pid: process.pid, startedAt: Date.now() };
|
|
248
238
|
try {
|
|
249
|
-
|
|
239
|
+
await Bun.write(lockPath, JSON.stringify(payload));
|
|
250
240
|
} catch {
|
|
251
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
}
|
|
283
|
+
})();
|
|
294
284
|
try {
|
|
295
|
-
writeLockInfo(lockPath
|
|
285
|
+
await writeLockInfo(lockPath);
|
|
296
286
|
return await handler();
|
|
297
287
|
} finally {
|
|
298
|
-
|
|
288
|
+
heartbeatRunning = false;
|
|
289
|
+
void heartbeat.catch(() => {}); // Don't await - let it die naturally
|
|
299
290
|
try {
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
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() -
|
|
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
|
-
|
|
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 =
|
|
333
|
+
const content = await Bun.file(infoPath).text();
|
|
345
334
|
const parsed = JSON.parse(content) as Partial<GatewayInfo>;
|
|
346
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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:
|
|
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
|
-
|
|
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", {
|