@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.
- package/CHANGELOG.md +21 -1
- 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 +51 -23
- package/scripts/format-prompts.ts +0 -1
- package/src/capability/context-file.ts +2 -3
- package/src/capability/extension-module.ts +2 -3
- package/src/capability/extension.ts +2 -3
- package/src/capability/fs.ts +20 -21
- package/src/capability/hook.ts +2 -3
- package/src/capability/index.ts +15 -16
- package/src/capability/instruction.ts +2 -3
- package/src/capability/mcp.ts +2 -3
- package/src/capability/prompt.ts +2 -3
- package/src/capability/rule.ts +2 -3
- package/src/capability/settings.ts +1 -2
- package/src/capability/skill.ts +2 -3
- package/src/capability/slash-command.ts +2 -3
- package/src/capability/ssh.ts +2 -3
- package/src/capability/system-prompt.ts +2 -3
- package/src/capability/tool.ts +2 -3
- 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 -21
- 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 +21 -23
- package/src/commit/agentic/fallback.ts +9 -9
- package/src/commit/agentic/index.ts +30 -38
- package/src/commit/agentic/state.ts +1 -6
- package/src/commit/agentic/tools/analyze-file.ts +15 -15
- package/src/commit/agentic/tools/git-file-diff.ts +3 -3
- package/src/commit/agentic/tools/git-hunk.ts +7 -7
- package/src/commit/agentic/tools/git-overview.ts +5 -5
- package/src/commit/agentic/tools/index.ts +14 -14
- package/src/commit/agentic/tools/propose-changelog.ts +6 -6
- package/src/commit/agentic/tools/propose-commit.ts +8 -8
- package/src/commit/agentic/tools/recent-commits.ts +2 -2
- package/src/commit/agentic/tools/split-commit.ts +19 -23
- package/src/commit/agentic/topo-sort.ts +1 -1
- package/src/commit/agentic/trivial.ts +3 -3
- package/src/commit/agentic/validation.ts +12 -12
- package/src/commit/analysis/conventional.ts +7 -11
- package/src/commit/analysis/index.ts +4 -4
- package/src/commit/analysis/scope.ts +4 -4
- package/src/commit/analysis/summary.ts +7 -9
- package/src/commit/analysis/validation.ts +1 -1
- package/src/commit/changelog/detect.ts +6 -6
- package/src/commit/changelog/generate.ts +7 -9
- package/src/commit/changelog/index.ts +13 -13
- package/src/commit/changelog/parse.ts +2 -2
- package/src/commit/cli.ts +1 -1
- package/src/commit/git/diff.ts +3 -3
- package/src/commit/git/index.ts +19 -24
- package/src/commit/index.ts +1 -1
- package/src/commit/map-reduce/index.ts +9 -9
- package/src/commit/map-reduce/map-phase.ts +19 -34
- package/src/commit/map-reduce/reduce-phase.ts +9 -11
- package/src/commit/message.ts +2 -2
- package/src/commit/model-selection.ts +3 -7
- package/src/commit/pipeline.ts +20 -22
- package/src/commit/utils/exclusions.ts +3 -3
- package/src/config/file-lock.ts +17 -7
- package/src/config/keybindings.ts +6 -8
- package/src/config/model-registry.ts +55 -37
- package/src/config/model-resolver.ts +18 -19
- package/src/config/prompt-templates.ts +11 -11
- package/src/config/settings-manager.ts +50 -34
- package/src/config.ts +60 -62
- 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 +16 -18
- 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 +17 -14
- package/src/extensibility/custom-commands/types.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +10 -11
- package/src/extensibility/custom-tools/types.ts +6 -7
- package/src/extensibility/custom-tools/wrapper.ts +2 -3
- package/src/extensibility/extensions/loader.ts +75 -53
- package/src/extensibility/extensions/runner.ts +11 -12
- package/src/extensibility/extensions/types.ts +19 -26
- package/src/extensibility/extensions/wrapper.ts +3 -4
- package/src/extensibility/hooks/index.ts +1 -1
- package/src/extensibility/hooks/loader.ts +8 -9
- package/src/extensibility/hooks/runner.ts +7 -8
- package/src/extensibility/hooks/tool-wrapper.ts +0 -1
- package/src/extensibility/hooks/types.ts +10 -17
- 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 +46 -46
- 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 +14 -10
- 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 +10 -16
- package/src/lsp/utils.ts +3 -3
- package/src/main.ts +55 -34
- 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 +3 -4
- 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 +60 -49
- package/src/modes/components/armin.ts +4 -5
- package/src/modes/components/assistant-message.ts +6 -6
- 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 +11 -16
- package/src/modes/components/tree-selector.ts +11 -11
- 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 +27 -29
- 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 -25
- package/src/modes/rpc/rpc-types.ts +3 -4
- package/src/modes/theme/mermaid-cache.ts +2 -2
- package/src/modes/theme/theme.ts +128 -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 +10 -11
- 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 +12 -13
- package/src/sdk.ts +60 -63
- package/src/session/agent-session.ts +83 -84
- package/src/session/agent-storage.ts +11 -11
- package/src/session/artifacts.ts +8 -9
- package/src/session/auth-storage.ts +25 -29
- 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 +2 -3
- 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 +27 -34
- 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 +33 -36
- package/src/task/output-manager.ts +3 -4
- package/src/task/parallel.ts +0 -1
- package/src/task/render.ts +19 -20
- package/src/task/subprocess-tool-registry.ts +1 -2
- package/src/task/worker-protocol.ts +3 -3
- package/src/task/worker.ts +32 -38
- package/src/task/worktree.ts +19 -19
- package/src/tools/ask.ts +8 -9
- package/src/tools/bash-interceptor.ts +1 -5
- package/src/tools/bash.ts +19 -18
- package/src/tools/calculator.ts +12 -12
- package/src/tools/complete.ts +3 -4
- package/src/tools/context.ts +2 -2
- package/src/tools/fetch.ts +23 -26
- package/src/tools/find.ts +15 -16
- package/src/tools/gemini-image.ts +14 -14
- package/src/tools/grep.ts +27 -27
- package/src/tools/index.ts +78 -56
- package/src/tools/list-limit.ts +1 -1
- package/src/tools/ls.ts +7 -7
- package/src/tools/notebook.ts +5 -5
- package/src/tools/output-meta.ts +3 -4
- package/src/tools/output-utils.ts +1 -1
- package/src/tools/path-utils.ts +5 -5
- package/src/tools/python.ts +36 -37
- package/src/tools/read.ts +23 -23
- package/src/tools/render-utils.ts +8 -9
- package/src/tools/renderers.ts +6 -7
- package/src/tools/review.ts +8 -11
- package/src/tools/ssh.ts +31 -30
- package/src/tools/todo-write.ts +13 -13
- package/src/tools/tool-errors.ts +3 -3
- package/src/tools/tool-result.ts +3 -8
- package/src/tools/write.ts +11 -16
- package/src/tui/code-cell.ts +3 -9
- package/src/tui/file-list.ts +3 -4
- package/src/tui/output-block.ts +1 -2
- package/src/tui/status-line.ts +2 -3
- package/src/tui/tree-list.ts +2 -3
- package/src/tui/types.ts +1 -2
- package/src/tui/utils.ts +2 -3
- 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 +4 -9
- 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 +16 -18
- package/scripts/generate-wasm-b64.ts +0 -24
- package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
- package/src/task/.executor.ts.kate-swp +0 -0
- package/src/vendor/photon/photon_rs_bg.wasm.b64.js +0 -1
package/src/ipy/executor.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
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 "
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
366
|
-
onDisplay:
|
|
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
|
|
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
|
|
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", {
|
package/src/ipy/kernel.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
|
-
import
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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;
|