@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.6
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 +69 -0
- package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
- package/dist/cli.js +4438 -3962
- package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
- package/dist/types/cli/args.d.ts +4 -1
- package/dist/types/cli/browser-relay-cli.d.ts +15 -0
- package/dist/types/cli/command-help.d.ts +99 -0
- package/dist/types/cli/gc-cli.d.ts +1 -0
- package/dist/types/cli/help-extra.d.ts +2 -0
- package/dist/types/cli/thinking-levels.d.ts +5 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/commands/browser-relay.d.ts +38 -0
- package/dist/types/commands/commit.d.ts +3 -0
- package/dist/types/commands/complete.d.ts +1 -1
- package/dist/types/commands/completions.d.ts +6 -0
- package/dist/types/commands/grep.d.ts +3 -0
- package/dist/types/commands/join.d.ts +4 -0
- package/dist/types/commands/launch-help.d.ts +171 -0
- package/dist/types/commands/launch.d.ts +5 -1
- package/dist/types/commands/models.d.ts +3 -0
- package/dist/types/commands/say.d.ts +10 -0
- package/dist/types/commands/worktree.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-discovery.d.ts +2 -2
- package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/models-config.d.ts +1 -0
- package/dist/types/config/service-tier.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +69 -21
- package/dist/types/cursor-bridge-tools.d.ts +1 -1
- package/dist/types/cursor.d.ts +7 -4
- package/dist/types/edit/index.d.ts +3 -3
- package/dist/types/edit/modes/replace.d.ts +22 -15
- package/dist/types/edit/renderer.d.ts +1 -1
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/spawn-options.d.ts +12 -16
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/launch/broker.d.ts +1 -1
- package/dist/types/launch/client.d.ts +15 -4
- package/dist/types/launch/ensure.d.ts +8 -0
- package/dist/types/launch/paths.d.ts +1 -1
- package/dist/types/launch/protocol.d.ts +19 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/mux/daemon.d.ts +15 -0
- package/dist/types/lsp/mux/protocol.d.ts +57 -0
- package/dist/types/lsp/mux/server.d.ts +18 -0
- package/dist/types/lsp/types.d.ts +22 -2
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mnemopi/embed-client.d.ts +1 -1
- package/dist/types/mnemopi/state.d.ts +0 -23
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/btw-panel.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/workflow.d.ts +2 -1
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/security/contracts/schemas.d.ts +405 -403
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/indexed-session-storage.d.ts +1 -0
- package/dist/types/session/launch-completion.d.ts +10 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-context.d.ts +2 -1
- package/dist/types/session/session-entries.d.ts +15 -1
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +23 -11
- package/dist/types/session/session-stats.d.ts +10 -0
- package/dist/types/session/session-storage.d.ts +15 -3
- package/dist/types/session/yield-queue.d.ts +6 -1
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/spawn-policy.d.ts +5 -0
- package/dist/types/thinking.d.ts +1 -7
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +2 -1
- package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
- package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
- package/dist/types/tools/browser/relay/kind.d.ts +19 -0
- package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
- package/dist/types/tools/browser/relay/server.d.ts +35 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -0
- package/dist/types/tools/computer/exposure.d.ts +9 -7
- package/dist/types/tools/computer/protocol.d.ts +70 -17
- package/dist/types/tools/computer/supervisor.d.ts +23 -10
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +42 -7
- package/dist/types/tools/computer-renderer.d.ts +7 -5
- package/dist/types/tools/computer.d.ts +25 -44
- package/dist/types/tools/context.d.ts +3 -0
- package/dist/types/tools/glob.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +1 -1
- package/dist/types/tools/index.d.ts +9 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/tools/xdev.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +2 -2
- package/dist/types/web/kagi.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/providers/anthropic.d.ts +1 -0
- package/dist/types/web/search/providers/base.d.ts +2 -0
- package/dist/types/web/search/providers/brave.d.ts +1 -0
- package/dist/types/web/search/providers/browser-page.d.ts +1 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -0
- package/dist/types/web/search/providers/exa.d.ts +1 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/dist/types/web/search/providers/jina.d.ts +1 -0
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -0
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +1 -0
- package/dist/types/web/search/providers/searxng.d.ts +1 -0
- package/dist/types/web/search/providers/tavily.d.ts +1 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +6 -9
- package/dist/types/web/search/providers/zai.d.ts +1 -0
- package/dist/types/web/search/types.d.ts +4 -0
- package/package.json +12 -12
- package/src/cli/args.ts +7 -87
- package/src/cli/browser-relay-cli.ts +119 -0
- package/src/cli/command-help.ts +103 -0
- package/src/cli/completion-gen.ts +5 -5
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gc-cli.ts +641 -21
- package/src/cli/help-extra.ts +89 -0
- package/src/cli/setup-cli.ts +13 -21
- package/src/cli/thinking-levels.ts +7 -0
- package/src/cli/update-cli.ts +26 -7
- package/src/cli-commands.ts +170 -33
- package/src/cli.ts +17 -5
- package/src/commands/acp.ts +3 -1
- package/src/commands/agents.ts +3 -2
- package/src/commands/auth-broker.ts +3 -2
- package/src/commands/auth-gateway.ts +3 -2
- package/src/commands/bench.ts +2 -3
- package/src/commands/browser-relay.ts +53 -0
- package/src/commands/cleanse.ts +2 -2
- package/src/commands/commit.ts +3 -2
- package/src/commands/complete.ts +2 -1
- package/src/commands/completions.ts +3 -2
- package/src/commands/config.ts +3 -2
- package/src/commands/dry-balance.ts +2 -2
- package/src/commands/gallery.ts +3 -2
- package/src/commands/gc.ts +3 -2
- package/src/commands/grep.ts +3 -2
- package/src/commands/grievances.ts +3 -2
- package/src/commands/install.ts +2 -2
- package/src/commands/join.ts +3 -2
- package/src/commands/launch-help.ts +116 -0
- package/src/commands/launch.ts +7 -191
- package/src/commands/models.ts +3 -2
- package/src/commands/plugin.ts +3 -2
- package/src/commands/read.ts +3 -2
- package/src/commands/say.ts +3 -2
- package/src/commands/setup.ts +3 -2
- package/src/commands/shell.ts +3 -2
- package/src/commands/ssh.ts +3 -2
- package/src/commands/stats.ts +3 -2
- package/src/commands/tiny-models.ts +2 -2
- package/src/commands/token.ts +2 -2
- package/src/commands/ttsr.ts +2 -2
- package/src/commands/update.ts +3 -2
- package/src/commands/usage.ts +3 -2
- package/src/commands/web-search.ts +3 -2
- package/src/commands/worktree.ts +3 -2
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-discovery.ts +44 -27
- package/src/config/model-registry.ts +19 -13
- package/src/config/models-config-schema-bundle.ts +9 -0
- package/src/config/resolve-config-value.ts +2 -1
- package/src/config/service-tier.ts +5 -0
- package/src/config/settings-schema.ts +72 -20
- package/src/config/settings.ts +1 -1
- package/src/cursor-bridge-tools.ts +1 -1
- package/src/cursor.ts +10 -14
- package/src/edit/diff.ts +17 -8
- package/src/edit/index.ts +20 -7
- package/src/edit/modes/replace.ts +93 -40
- package/src/edit/renderer.ts +1 -1
- package/src/edit/streaming.ts +14 -17
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +24 -29
- package/src/export/html/index.ts +10 -3
- package/src/export/html/template.js +8 -26
- package/src/export/share.ts +4 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
- package/src/extensibility/skills.ts +11 -0
- package/src/extensibility/utils.ts +10 -2
- package/src/launch/broker.ts +223 -7
- package/src/launch/client.ts +192 -21
- package/src/launch/ensure.ts +73 -0
- package/src/launch/paths.ts +1 -1
- package/src/launch/protocol.ts +54 -2
- package/src/lsp/client.ts +23 -2
- package/src/lsp/index.ts +8 -0
- package/src/lsp/mux/daemon.ts +348 -0
- package/src/lsp/mux/protocol.ts +96 -0
- package/src/lsp/mux/server.ts +797 -0
- package/src/lsp/types.ts +28 -2
- package/src/main.ts +20 -8
- package/src/mcp/config-writer.ts +1 -1
- package/src/mcp/oauth-flow.ts +11 -1
- package/src/mnemopi/embed-client.ts +49 -2
- package/src/mnemopi/state.ts +57 -23
- package/src/modes/components/assistant-message.ts +11 -0
- package/src/modes/components/btw-panel.ts +41 -4
- package/src/modes/components/chat-transcript-builder.ts +4 -0
- package/src/modes/components/custom-editor.ts +9 -0
- package/src/modes/components/read-tool-group.ts +10 -0
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
- package/src/modes/components/tool-execution.ts +8 -0
- package/src/modes/components/tree-selector.ts +46 -48
- package/src/modes/controllers/btw-controller.ts +55 -7
- package/src/modes/controllers/command-controller.ts +31 -0
- package/src/modes/controllers/event-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +63 -8
- package/src/modes/controllers/mcp-command-controller.ts +11 -5
- package/src/modes/controllers/selector-controller.ts +20 -0
- package/src/modes/interactive-mode.ts +18 -2
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +6 -10
- package/src/modes/workflow.ts +8 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/session/launch-completion.md +1 -0
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +26 -52
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/computer.md +21 -21
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/replace.md +5 -4
- package/src/prompts/tools/task.md +4 -4
- package/src/sdk.ts +49 -6
- package/src/secrets/index.ts +1 -1
- package/src/security/contracts/schemas.ts +205 -183
- package/src/security/contracts/validation.ts +5 -1
- package/src/security/store.ts +2 -2
- package/src/session/agent-session-types.ts +9 -1
- package/src/session/agent-session.ts +204 -15
- package/src/session/indexed-session-storage.ts +9 -0
- package/src/session/launch-completion.ts +37 -0
- package/src/session/prewalk.ts +24 -2
- package/src/session/session-advisors.ts +2 -2
- package/src/session/session-context.ts +42 -12
- package/src/session/session-dump-format.ts +1 -1
- package/src/session/session-entries.ts +17 -1
- package/src/session/session-maintenance.ts +38 -4
- package/src/session/session-manager.ts +123 -67
- package/src/session/session-paths.ts +118 -55
- package/src/session/session-stats.ts +48 -3
- package/src/session/session-storage.ts +30 -22
- package/src/session/stream-guards.ts +1 -1
- package/src/session/yield-queue.ts +121 -16
- package/src/slash-commands/builtin-registry.ts +37 -24
- package/src/system-prompt.ts +8 -3
- package/src/task/index.ts +23 -8
- package/src/task/spawn-policy.ts +14 -0
- package/src/thinking.ts +2 -8
- package/src/tools/ast-grep.ts +10 -4
- package/src/tools/browser/cmux/cmux-tab.ts +97 -14
- package/src/tools/browser/cmux/rpc.ts +2 -8
- package/src/tools/browser/registry.ts +53 -2
- package/src/tools/browser/relay/bridge.ts +945 -0
- package/src/tools/browser/relay/daemon.ts +117 -0
- package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
- package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
- package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
- package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
- package/src/tools/browser/relay/kind.ts +41 -0
- package/src/tools/browser/relay/protocol.ts +54 -0
- package/src/tools/browser/relay/server.ts +141 -0
- package/src/tools/browser/render.ts +4 -1
- package/src/tools/browser/shared-daemon.ts +7 -58
- package/src/tools/browser/tab-supervisor.ts +5 -4
- package/src/tools/browser/tab-worker.ts +33 -12
- package/src/tools/browser.ts +26 -1
- package/src/tools/computer/exposure.ts +11 -35
- package/src/tools/computer/protocol.ts +51 -10
- package/src/tools/computer/supervisor.ts +227 -124
- package/src/tools/computer/worker-entry.ts +11 -6
- package/src/tools/computer/worker.ts +695 -81
- package/src/tools/computer-renderer.ts +104 -65
- package/src/tools/computer.ts +134 -428
- package/src/tools/context.ts +3 -0
- package/src/tools/glob.ts +9 -2
- package/src/tools/grep.ts +12 -6
- package/src/tools/hub/launch.ts +122 -6
- package/src/tools/index.ts +9 -0
- package/src/tools/inspect-image.ts +32 -6
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/xdev.ts +29 -2
- package/src/utils/clipboard.ts +40 -14
- package/src/utils/command-args.ts +26 -28
- package/src/utils/tool-choice.ts +0 -13
- package/src/web/kagi.ts +2 -1
- package/src/web/search/index.ts +18 -2
- package/src/web/search/providers/anthropic.ts +4 -1
- package/src/web/search/providers/base.ts +2 -0
- package/src/web/search/providers/brave.ts +3 -1
- package/src/web/search/providers/browser-page.ts +6 -4
- package/src/web/search/providers/codex.ts +4 -1
- package/src/web/search/providers/duckduckgo.ts +2 -1
- package/src/web/search/providers/ecosia.ts +2 -1
- package/src/web/search/providers/exa.ts +4 -2
- package/src/web/search/providers/firecrawl.ts +3 -1
- package/src/web/search/providers/gemini.ts +8 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +12 -2
- package/src/web/search/providers/kagi.ts +3 -0
- package/src/web/search/providers/kimi.ts +5 -1
- package/src/web/search/providers/mojeek.ts +2 -1
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +6 -3
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/searxng.ts +11 -5
- package/src/web/search/providers/startpage.ts +10 -4
- package/src/web/search/providers/synthetic.ts +11 -5
- package/src/web/search/providers/tavily.ts +3 -1
- package/src/web/search/providers/tinyfish.ts +3 -1
- package/src/web/search/providers/utils.ts +12 -10
- package/src/web/search/providers/xai.ts +2 -2
- package/src/web/search/providers/zai.ts +18 -3
- package/src/web/search/types.ts +6 -0
- package/dist/types/config/file-lock.d.ts +0 -29
- package/src/config/file-lock.ts +0 -164
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import { isRecord, logger, postmortem, ptree, setProcessName } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { MessageFramer } from "../../jsonrpc/message-framing";
|
|
5
|
+
import type { LspJsonRpcId, LspJsonRpcNotification, LspJsonRpcRequest, LspJsonRpcResponse } from "../types";
|
|
6
|
+
import {
|
|
7
|
+
LSP_MUX_PROJECT_DIR_ENV,
|
|
8
|
+
LSP_MUX_SOCKET_ENV,
|
|
9
|
+
lspMuxReadyBanner,
|
|
10
|
+
MUX_CONNECT_METHOD,
|
|
11
|
+
MUX_PING_METHOD,
|
|
12
|
+
MUX_PING_RESULT,
|
|
13
|
+
MUX_RESTART_METHOD,
|
|
14
|
+
type MuxConnectParams,
|
|
15
|
+
type MuxConnectResult,
|
|
16
|
+
muxServerKey,
|
|
17
|
+
} from "./protocol";
|
|
18
|
+
|
|
19
|
+
const SERVER_LINGER_MS = 5 * 60 * 1_000;
|
|
20
|
+
const MUX_IDLE_MS = 15 * 60 * 1_000;
|
|
21
|
+
const SHUTDOWN_BUDGET_MS = 2_000;
|
|
22
|
+
|
|
23
|
+
type RpcMessage = LspJsonRpcRequest | LspJsonRpcResponse | LspJsonRpcNotification;
|
|
24
|
+
|
|
25
|
+
interface SessionDocumentVersion {
|
|
26
|
+
clientVersion: number;
|
|
27
|
+
serverVersion: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface DocumentRecord {
|
|
31
|
+
serverVersion: number;
|
|
32
|
+
perSession: Map<Session, SessionDocumentVersion>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ForwardedRequest {
|
|
36
|
+
session?: Session;
|
|
37
|
+
originalId?: LspJsonRpcId;
|
|
38
|
+
drop?: boolean;
|
|
39
|
+
initialize?: boolean;
|
|
40
|
+
resolveInternal?: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ClientRequestPending {
|
|
44
|
+
relay: boolean;
|
|
45
|
+
serverId?: LspJsonRpcId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Registration {
|
|
49
|
+
id: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface RegistrationBatch {
|
|
54
|
+
registrations: Registration[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ProgressParams {
|
|
58
|
+
token: string | number;
|
|
59
|
+
value?: { kind?: string; [key: string]: unknown };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface DiagnosticsParams {
|
|
63
|
+
uri: string;
|
|
64
|
+
version?: number | null;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface TextDocumentParams {
|
|
69
|
+
textDocument: { uri: string; version: number; text?: string; [key: string]: unknown };
|
|
70
|
+
contentChanges?: unknown;
|
|
71
|
+
[key: string]: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class Session {
|
|
75
|
+
readonly socket: net.Socket;
|
|
76
|
+
readonly framer = new MessageFramer(Buffer.alloc(0));
|
|
77
|
+
readonly openUris = new Set<string>();
|
|
78
|
+
readonly forwardedIds = new Map<LspJsonRpcId, LspJsonRpcId>();
|
|
79
|
+
readonly pendingClientRequests = new Map<string, ClientRequestPending>();
|
|
80
|
+
server?: ServerInstance;
|
|
81
|
+
boundKey?: string;
|
|
82
|
+
initialized = false;
|
|
83
|
+
closed = false;
|
|
84
|
+
lastActivity = Date.now();
|
|
85
|
+
|
|
86
|
+
constructor(socket: net.Socket) {
|
|
87
|
+
this.socket = socket;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class ServerInstance {
|
|
92
|
+
readonly key: string;
|
|
93
|
+
readonly proc: ptree.ChildProcess<"pipe">;
|
|
94
|
+
readonly sessions = new Set<Session>();
|
|
95
|
+
readonly documents = new Map<string, DocumentRecord>();
|
|
96
|
+
readonly diagnostics = new Map<string, DiagnosticsParams>();
|
|
97
|
+
readonly registrations: RegistrationBatch[] = [];
|
|
98
|
+
readonly progress = new Map<string | number, ProgressParams>();
|
|
99
|
+
readonly pending = new Map<LspJsonRpcId, ForwardedRequest>();
|
|
100
|
+
readonly initializeWaiters = new Map<Session, LspJsonRpcId>();
|
|
101
|
+
writeQueue: Promise<void> = Promise.resolve();
|
|
102
|
+
initializeResult: unknown = undefined;
|
|
103
|
+
initializeCached = false;
|
|
104
|
+
initializeInFlight = false;
|
|
105
|
+
initializedSent = false;
|
|
106
|
+
nextId = 1;
|
|
107
|
+
nextClientId = 1;
|
|
108
|
+
lingerTimer?: NodeJS.Timeout;
|
|
109
|
+
stopping = false;
|
|
110
|
+
|
|
111
|
+
constructor(key: string, params: MuxConnectParams) {
|
|
112
|
+
this.key = key;
|
|
113
|
+
this.proc = ptree.spawn([params.command, ...params.args], {
|
|
114
|
+
cwd: params.cwd,
|
|
115
|
+
stdin: "pipe",
|
|
116
|
+
env: { ...Bun.env, ...params.env },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
muxId(): number {
|
|
121
|
+
return this.nextId++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
clientId(): string {
|
|
125
|
+
return `mux:${this.nextClientId++}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function hasMethod(message: RpcMessage): message is LspJsonRpcRequest | LspJsonRpcNotification {
|
|
130
|
+
return "method" in message && typeof message.method === "string";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function hasRequestId(message: LspJsonRpcRequest | LspJsonRpcNotification): message is LspJsonRpcRequest {
|
|
134
|
+
return "id" in message && (typeof message.id === "number" || typeof message.id === "string");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function frame(message: RpcMessage): string {
|
|
138
|
+
const body = JSON.stringify(message);
|
|
139
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function rpcResult(id: LspJsonRpcId, result: unknown): LspJsonRpcResponse {
|
|
143
|
+
return { jsonrpc: "2.0", id, result };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function rpcError(id: LspJsonRpcId, code: number, message: string): LspJsonRpcResponse {
|
|
147
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseConnectParams(params: unknown): MuxConnectParams | undefined {
|
|
151
|
+
if (!isRecord(params) || typeof params.command !== "string" || typeof params.cwd !== "string") return undefined;
|
|
152
|
+
if (!Array.isArray(params.args) || !params.args.every(arg => typeof arg === "string")) return undefined;
|
|
153
|
+
if (params.env !== undefined) {
|
|
154
|
+
if (!isRecord(params.env)) return undefined;
|
|
155
|
+
for (const key in params.env) if (typeof params.env[key] !== "string") return undefined;
|
|
156
|
+
}
|
|
157
|
+
return params as unknown as MuxConnectParams;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseDocumentParams(params: unknown): TextDocumentParams | undefined {
|
|
161
|
+
if (!isRecord(params) || !isRecord(params.textDocument)) return undefined;
|
|
162
|
+
if (typeof params.textDocument.uri !== "string" || typeof params.textDocument.version !== "number") return undefined;
|
|
163
|
+
return params as unknown as TextDocumentParams;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseUri(params: unknown): string | undefined {
|
|
167
|
+
if (!isRecord(params) || !isRecord(params.textDocument)) return undefined;
|
|
168
|
+
return typeof params.textDocument.uri === "string" ? params.textDocument.uri : undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseDiagnostics(params: unknown): DiagnosticsParams | undefined {
|
|
172
|
+
if (!isRecord(params) || typeof params.uri !== "string") return undefined;
|
|
173
|
+
return params as DiagnosticsParams;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseProgress(params: unknown): ProgressParams | undefined {
|
|
177
|
+
if (!isRecord(params) || (typeof params.token !== "string" && typeof params.token !== "number")) return undefined;
|
|
178
|
+
return params as unknown as ProgressParams;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cloneParams<T>(params: T): T {
|
|
182
|
+
return structuredClone(params);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Broker-owned, in-process-testable multiplexer for shared language-server children.
|
|
187
|
+
*/
|
|
188
|
+
export class LspMuxServer {
|
|
189
|
+
/** Called after the mux has had no connected sessions for its idle grace period. */
|
|
190
|
+
onIdle?: () => void;
|
|
191
|
+
readonly #servers = new Map<string, ServerInstance>();
|
|
192
|
+
readonly #sessions = new Set<Session>();
|
|
193
|
+
#netServer?: net.Server;
|
|
194
|
+
#endpoint?: string;
|
|
195
|
+
#idleTimer?: NodeJS.Timeout;
|
|
196
|
+
#activityClock = Date.now();
|
|
197
|
+
#shuttingDown = false;
|
|
198
|
+
#shutdownPromise?: Promise<void>;
|
|
199
|
+
|
|
200
|
+
/** Number of currently connected mux links, including unbound ping links. */
|
|
201
|
+
get sessionCount(): number {
|
|
202
|
+
return this.#sessions.size;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Keys of currently live shared language-server children. */
|
|
206
|
+
get serverKeys(): string[] {
|
|
207
|
+
return [...this.#servers.keys()];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Listen for Content-Length framed mux links at a Unix socket or named pipe. */
|
|
211
|
+
async listen(endpoint: string): Promise<void> {
|
|
212
|
+
if (this.#netServer) throw new Error("LSP mux is already listening");
|
|
213
|
+
if (process.platform !== "win32") await this.#clearStaleSocket(endpoint);
|
|
214
|
+
const server = net.createServer(socket => this.#accept(socket));
|
|
215
|
+
this.#netServer = server;
|
|
216
|
+
this.#endpoint = endpoint;
|
|
217
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
218
|
+
const onError = (error: Error) => reject(error);
|
|
219
|
+
server.once("error", onError);
|
|
220
|
+
server.listen(endpoint, () => {
|
|
221
|
+
server.off("error", onError);
|
|
222
|
+
resolve();
|
|
223
|
+
});
|
|
224
|
+
await promise;
|
|
225
|
+
this.#armMuxIdle();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Gracefully close all children, links, and the listening endpoint. */
|
|
229
|
+
async shutdown(): Promise<void> {
|
|
230
|
+
this.#shutdownPromise ??= this.#performShutdown();
|
|
231
|
+
await this.#shutdownPromise;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async #performShutdown(): Promise<void> {
|
|
235
|
+
this.#shuttingDown = true;
|
|
236
|
+
clearTimeout(this.#idleTimer);
|
|
237
|
+
for (const session of [...this.#sessions]) session.socket.destroy();
|
|
238
|
+
await Promise.all([...this.#servers.values()].map(server => this.#stopServer(server)));
|
|
239
|
+
const listener = this.#netServer;
|
|
240
|
+
this.#netServer = undefined;
|
|
241
|
+
if (listener) {
|
|
242
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
243
|
+
listener.close(() => resolve());
|
|
244
|
+
await promise;
|
|
245
|
+
}
|
|
246
|
+
if (process.platform !== "win32" && this.#endpoint) {
|
|
247
|
+
try {
|
|
248
|
+
await fs.unlink(this.#endpoint);
|
|
249
|
+
} catch {
|
|
250
|
+
// The socket may already have been removed by process cleanup.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async #clearStaleSocket(endpoint: string): Promise<void> {
|
|
256
|
+
try {
|
|
257
|
+
await fs.stat(endpoint);
|
|
258
|
+
} catch {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const alive = await this.#probe(endpoint);
|
|
262
|
+
if (alive) throw new Error(`LSP mux already listening on ${endpoint}`);
|
|
263
|
+
try {
|
|
264
|
+
await fs.unlink(endpoint);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
logger.warn("Failed to remove stale LSP mux socket", { endpoint, error: String(error) });
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async #probe(endpoint: string): Promise<boolean> {
|
|
272
|
+
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
273
|
+
const socket = net.createConnection(endpoint);
|
|
274
|
+
socket.once("connect", () => {
|
|
275
|
+
socket.destroy();
|
|
276
|
+
resolve(true);
|
|
277
|
+
});
|
|
278
|
+
socket.once("error", () => {
|
|
279
|
+
socket.destroy();
|
|
280
|
+
resolve(false);
|
|
281
|
+
});
|
|
282
|
+
return promise;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#accept(socket: net.Socket): void {
|
|
286
|
+
const session = new Session(socket);
|
|
287
|
+
this.#sessions.add(session);
|
|
288
|
+
this.#disarmMuxIdle();
|
|
289
|
+
socket.on("data", chunk => {
|
|
290
|
+
session.framer.push(Buffer.from(chunk));
|
|
291
|
+
for (const text of session.framer.drain(header => {
|
|
292
|
+
logger.warn("LSP mux client framing resync", { header: header.slice(0, 200) });
|
|
293
|
+
})) {
|
|
294
|
+
try {
|
|
295
|
+
const parsed: unknown = JSON.parse(text);
|
|
296
|
+
if (!isRecord(parsed) || parsed.jsonrpc !== "2.0") throw new Error("invalid JSON-RPC message");
|
|
297
|
+
void this.#fromSession(session, parsed as unknown as RpcMessage).catch(error => {
|
|
298
|
+
logger.warn("LSP mux client message handling failed", { error: String(error) });
|
|
299
|
+
});
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logger.warn("LSP mux client sent malformed JSON", { error: String(error) });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
socket.on("error", error => logger.warn("LSP mux session socket error", { error: error.message }));
|
|
306
|
+
socket.on("close", () => void this.#closeSession(session));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async #fromSession(session: Session, message: RpcMessage): Promise<void> {
|
|
310
|
+
if (session.closed) return;
|
|
311
|
+
this.#activityClock = Math.max(Date.now(), this.#activityClock + 1);
|
|
312
|
+
session.lastActivity = this.#activityClock;
|
|
313
|
+
if (!hasMethod(message)) {
|
|
314
|
+
this.#handleClientResponse(session, message);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const request = hasRequestId(message);
|
|
318
|
+
if (message.method === MUX_PING_METHOD && request) {
|
|
319
|
+
this.#sendSession(session, rpcResult(message.id, MUX_PING_RESULT));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (message.method === MUX_CONNECT_METHOD && request) {
|
|
323
|
+
if (session.server) {
|
|
324
|
+
this.#sendSession(session, rpcError(message.id, -32600, "session already bound"));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const params = parseConnectParams(message.params);
|
|
328
|
+
if (!params) {
|
|
329
|
+
this.#sendSession(session, rpcError(message.id, -32602, "invalid mux connect params"));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const key = muxServerKey(params.command, params.cwd);
|
|
333
|
+
let server = this.#servers.get(key);
|
|
334
|
+
if (server && server.proc.exitCode !== null) {
|
|
335
|
+
this.#serverExited(server);
|
|
336
|
+
server = undefined;
|
|
337
|
+
}
|
|
338
|
+
let spawned = false;
|
|
339
|
+
if (!server || server.stopping) {
|
|
340
|
+
server = this.#spawnServer(key, params);
|
|
341
|
+
spawned = true;
|
|
342
|
+
}
|
|
343
|
+
if (server.lingerTimer) clearTimeout(server.lingerTimer);
|
|
344
|
+
server.lingerTimer = undefined;
|
|
345
|
+
server.sessions.add(session);
|
|
346
|
+
session.server = server;
|
|
347
|
+
session.boundKey = key;
|
|
348
|
+
const result: MuxConnectResult = { key, spawned, pid: server.proc.pid };
|
|
349
|
+
this.#sendSession(session, rpcResult(message.id, result));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const server = session.server;
|
|
353
|
+
if (!server) {
|
|
354
|
+
if (request) this.#sendSession(session, rpcError(message.id, -32002, "muxConnect must be first"));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (message.method === MUX_RESTART_METHOD && !request) {
|
|
358
|
+
this.#killServer(server);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (message.method === "initialize" && request) {
|
|
362
|
+
await this.#initialize(session, server, message);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (message.method === "initialized" && !request) {
|
|
366
|
+
if (!server.initializedSent) {
|
|
367
|
+
server.initializedSent = true;
|
|
368
|
+
await this.#writeServer(server, message);
|
|
369
|
+
} else {
|
|
370
|
+
this.#replayState(session, server);
|
|
371
|
+
}
|
|
372
|
+
session.initialized = true;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (message.method === "shutdown" && request) {
|
|
376
|
+
this.#sendSession(session, rpcResult(message.id, null));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (message.method === "exit" && !request) {
|
|
380
|
+
session.socket.destroy();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (message.method === "textDocument/didOpen" && !request) {
|
|
384
|
+
await this.#didOpen(session, server, message);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (message.method === "textDocument/didChange" && !request) {
|
|
388
|
+
await this.#didChange(session, server, message);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (message.method === "textDocument/didClose" && !request) {
|
|
392
|
+
await this.#didClose(session, server, message);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (message.method === "$/cancelRequest" && !request) {
|
|
396
|
+
if (!isRecord(message.params)) return;
|
|
397
|
+
const originalId = message.params.id;
|
|
398
|
+
if (typeof originalId !== "string" && typeof originalId !== "number") return;
|
|
399
|
+
const muxId = session.forwardedIds.get(originalId);
|
|
400
|
+
if (muxId !== undefined)
|
|
401
|
+
await this.#writeServer(server, { ...message, params: { ...message.params, id: muxId } });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (request) {
|
|
405
|
+
const muxId = server.muxId();
|
|
406
|
+
server.pending.set(muxId, { session, originalId: message.id });
|
|
407
|
+
session.forwardedIds.set(message.id, muxId);
|
|
408
|
+
await this.#writeServer(server, { ...message, id: muxId });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await this.#writeServer(server, message);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async #initialize(session: Session, server: ServerInstance, message: LspJsonRpcRequest): Promise<void> {
|
|
415
|
+
if (server.initializeCached) {
|
|
416
|
+
this.#sendSession(session, rpcResult(message.id, server.initializeResult));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
server.initializeWaiters.set(session, message.id);
|
|
420
|
+
if (server.initializeInFlight) return;
|
|
421
|
+
server.initializeInFlight = true;
|
|
422
|
+
const muxId = server.muxId();
|
|
423
|
+
server.pending.set(muxId, { initialize: true });
|
|
424
|
+
const params = isRecord(message.params)
|
|
425
|
+
? { ...message.params, processId: process.pid }
|
|
426
|
+
: { processId: process.pid };
|
|
427
|
+
// Language servers commonly self-terminate with their advertised client pid.
|
|
428
|
+
await this.#writeServer(server, { ...message, id: muxId, params });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async #didOpen(session: Session, server: ServerInstance, message: LspJsonRpcNotification): Promise<void> {
|
|
432
|
+
const params = parseDocumentParams(message.params);
|
|
433
|
+
if (!params) return;
|
|
434
|
+
const uri = params.textDocument.uri;
|
|
435
|
+
const clientVersion = params.textDocument.version;
|
|
436
|
+
session.openUris.add(uri);
|
|
437
|
+
const existing = server.documents.get(uri);
|
|
438
|
+
if (!existing) {
|
|
439
|
+
server.documents.set(uri, {
|
|
440
|
+
serverVersion: clientVersion,
|
|
441
|
+
perSession: new Map([[session, { clientVersion, serverVersion: clientVersion }]]),
|
|
442
|
+
});
|
|
443
|
+
await this.#writeServer(server, message);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
existing.serverVersion = Math.max(existing.serverVersion + 1, clientVersion);
|
|
447
|
+
existing.perSession.set(session, { clientVersion, serverVersion: existing.serverVersion });
|
|
448
|
+
await this.#writeServer(server, {
|
|
449
|
+
jsonrpc: "2.0",
|
|
450
|
+
method: "textDocument/didChange",
|
|
451
|
+
params: {
|
|
452
|
+
textDocument: { uri, version: existing.serverVersion },
|
|
453
|
+
contentChanges: [{ text: params.textDocument.text ?? "" }],
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async #didChange(session: Session, server: ServerInstance, message: LspJsonRpcNotification): Promise<void> {
|
|
459
|
+
const params = parseDocumentParams(message.params);
|
|
460
|
+
if (!params) return;
|
|
461
|
+
const uri = params.textDocument.uri;
|
|
462
|
+
const record = server.documents.get(uri);
|
|
463
|
+
if (!record) {
|
|
464
|
+
await this.#writeServer(server, message);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const clientVersion = params.textDocument.version;
|
|
468
|
+
record.serverVersion = Math.max(record.serverVersion + 1, clientVersion);
|
|
469
|
+
record.perSession.set(session, { clientVersion, serverVersion: record.serverVersion });
|
|
470
|
+
// Map each client's version stream into the one monotonically increasing server stream.
|
|
471
|
+
await this.#writeServer(server, {
|
|
472
|
+
...message,
|
|
473
|
+
params: { ...params, textDocument: { ...params.textDocument, version: record.serverVersion } },
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async #didClose(session: Session, server: ServerInstance, message: LspJsonRpcNotification): Promise<void> {
|
|
478
|
+
const uri = parseUri(message.params);
|
|
479
|
+
if (!uri) return;
|
|
480
|
+
session.openUris.delete(uri);
|
|
481
|
+
const record = server.documents.get(uri);
|
|
482
|
+
if (!record) return;
|
|
483
|
+
record.perSession.delete(session);
|
|
484
|
+
if (record.perSession.size > 0) return;
|
|
485
|
+
server.documents.delete(uri);
|
|
486
|
+
await this.#writeServer(server, message);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#spawnServer(key: string, params: MuxConnectParams): ServerInstance {
|
|
490
|
+
const server = new ServerInstance(key, params);
|
|
491
|
+
this.#servers.set(key, server);
|
|
492
|
+
void this.#readServer(server);
|
|
493
|
+
server.proc.exited.then(
|
|
494
|
+
() => this.#serverExited(server),
|
|
495
|
+
() => this.#serverExited(server),
|
|
496
|
+
);
|
|
497
|
+
return server;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async #readServer(server: ServerInstance): Promise<void> {
|
|
501
|
+
const reader = server.proc.stdout.getReader();
|
|
502
|
+
const framer = new MessageFramer(Buffer.alloc(0));
|
|
503
|
+
try {
|
|
504
|
+
while (true) {
|
|
505
|
+
const { done, value } = await reader.read();
|
|
506
|
+
if (done) break;
|
|
507
|
+
framer.push(Buffer.from(value));
|
|
508
|
+
for (const text of framer.drain(header => {
|
|
509
|
+
logger.warn("LSP mux server framing resync", { server: server.key, header: header.slice(0, 200) });
|
|
510
|
+
})) {
|
|
511
|
+
try {
|
|
512
|
+
const parsed: unknown = JSON.parse(text);
|
|
513
|
+
if (!isRecord(parsed) || parsed.jsonrpc !== "2.0") throw new Error("invalid JSON-RPC message");
|
|
514
|
+
await this.#fromServer(server, parsed as unknown as RpcMessage);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
logger.warn("LSP mux server message handling failed", { server: server.key, error: String(error) });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
logger.warn("LSP mux server reader failed", { server: server.key, error: String(error) });
|
|
522
|
+
} finally {
|
|
523
|
+
reader.releaseLock();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async #fromServer(server: ServerInstance, message: RpcMessage): Promise<void> {
|
|
528
|
+
if (!hasMethod(message)) {
|
|
529
|
+
this.#handleServerResponse(server, message);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (hasRequestId(message)) {
|
|
533
|
+
await this.#handleServerRequest(server, message);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (message.method === "textDocument/publishDiagnostics") {
|
|
537
|
+
const params = parseDiagnostics(message.params);
|
|
538
|
+
if (!params) return;
|
|
539
|
+
server.diagnostics.set(params.uri, cloneParams(params));
|
|
540
|
+
for (const session of server.sessions) if (session.initialized) this.#sendDiagnostics(session, server, params);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (message.method === "$/progress") {
|
|
544
|
+
const params = parseProgress(message.params);
|
|
545
|
+
if (params) {
|
|
546
|
+
if (params.value?.kind === "begin") server.progress.set(params.token, cloneParams(params));
|
|
547
|
+
else if (params.value?.kind === "end") server.progress.delete(params.token);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
for (const session of server.sessions) if (session.initialized) this.#sendSession(session, message);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#handleServerResponse(server: ServerInstance, message: LspJsonRpcResponse): void {
|
|
554
|
+
if (message.id === undefined) return;
|
|
555
|
+
const pending = server.pending.get(message.id);
|
|
556
|
+
if (!pending) return;
|
|
557
|
+
server.pending.delete(message.id);
|
|
558
|
+
if (pending.resolveInternal) {
|
|
559
|
+
pending.resolveInternal();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (pending.initialize) {
|
|
563
|
+
server.initializeInFlight = false;
|
|
564
|
+
if (message.error) {
|
|
565
|
+
for (const [session, id] of server.initializeWaiters) this.#sendSession(session, { ...message, id });
|
|
566
|
+
server.initializeWaiters.clear();
|
|
567
|
+
this.#killServer(server);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
server.initializeResult = message.result;
|
|
571
|
+
server.initializeCached = true;
|
|
572
|
+
for (const [session, id] of server.initializeWaiters)
|
|
573
|
+
this.#sendSession(session, rpcResult(id, message.result));
|
|
574
|
+
server.initializeWaiters.clear();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const session = pending.session;
|
|
578
|
+
if (!session || session.closed || pending.drop || pending.originalId === undefined) return;
|
|
579
|
+
session.forwardedIds.delete(pending.originalId);
|
|
580
|
+
this.#sendSession(session, { ...message, id: pending.originalId });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async #handleServerRequest(server: ServerInstance, message: LspJsonRpcRequest): Promise<void> {
|
|
584
|
+
if (
|
|
585
|
+
message.method === "client/registerCapability" ||
|
|
586
|
+
message.method === "client/unregisterCapability" ||
|
|
587
|
+
message.method === "window/workDoneProgress/create"
|
|
588
|
+
) {
|
|
589
|
+
if (message.method === "client/registerCapability" && isRecord(message.params)) {
|
|
590
|
+
const registrations = message.params.registrations;
|
|
591
|
+
if (Array.isArray(registrations)) {
|
|
592
|
+
const valid = registrations.filter(
|
|
593
|
+
(registration): registration is Registration =>
|
|
594
|
+
isRecord(registration) && typeof registration.id === "string",
|
|
595
|
+
);
|
|
596
|
+
server.registrations.push({ registrations: cloneParams(valid) });
|
|
597
|
+
}
|
|
598
|
+
} else if (message.method === "client/unregisterCapability" && isRecord(message.params)) {
|
|
599
|
+
const raw = message.params.unregisterations ?? message.params.unregistrations;
|
|
600
|
+
if (Array.isArray(raw)) {
|
|
601
|
+
const ids = new Set(
|
|
602
|
+
raw.flatMap(item => (isRecord(item) && typeof item.id === "string" ? [item.id] : [])),
|
|
603
|
+
);
|
|
604
|
+
for (const batch of server.registrations)
|
|
605
|
+
batch.registrations = batch.registrations.filter(item => !ids.has(item.id));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
await this.#writeServer(server, rpcResult(message.id, null));
|
|
609
|
+
for (const session of server.sessions) {
|
|
610
|
+
if (!session.initialized) continue;
|
|
611
|
+
this.#forwardNoRelay(session, server, message);
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// Client-side effects such as applyEdit must reach exactly one, most recently active omp.
|
|
616
|
+
let focus: Session | undefined;
|
|
617
|
+
for (const session of server.sessions) {
|
|
618
|
+
if (!focus || session.lastActivity > focus.lastActivity) focus = session;
|
|
619
|
+
}
|
|
620
|
+
if (!focus) {
|
|
621
|
+
await this.#writeServer(server, rpcError(message.id, -32601, "no client attached"));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const id = server.clientId();
|
|
625
|
+
focus.pendingClientRequests.set(id, { relay: true, serverId: message.id });
|
|
626
|
+
this.#sendSession(focus, { ...message, id });
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
#forwardNoRelay(session: Session, server: ServerInstance, message: LspJsonRpcRequest): void {
|
|
630
|
+
const id = server.clientId();
|
|
631
|
+
session.pendingClientRequests.set(id, { relay: false });
|
|
632
|
+
this.#sendSession(session, { ...message, id });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#handleClientResponse(session: Session, message: LspJsonRpcResponse): void {
|
|
636
|
+
if (typeof message.id !== "string") return;
|
|
637
|
+
const pending = session.pendingClientRequests.get(message.id);
|
|
638
|
+
if (!pending) return;
|
|
639
|
+
session.pendingClientRequests.delete(message.id);
|
|
640
|
+
if (!pending.relay || pending.serverId === undefined || !session.server) return;
|
|
641
|
+
void this.#writeServer(session.server, { ...message, id: pending.serverId });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#sendDiagnostics(session: Session, server: ServerInstance, params: DiagnosticsParams): void {
|
|
645
|
+
const rewritten = cloneParams(params);
|
|
646
|
+
const version = server.documents.get(params.uri)?.perSession.get(session);
|
|
647
|
+
if (params.version !== undefined && version && params.version === version.serverVersion)
|
|
648
|
+
rewritten.version = version.clientVersion;
|
|
649
|
+
else delete rewritten.version;
|
|
650
|
+
this.#sendSession(session, { jsonrpc: "2.0", method: "textDocument/publishDiagnostics", params: rewritten });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#replayState(session: Session, server: ServerInstance): void {
|
|
654
|
+
for (const batch of server.registrations) {
|
|
655
|
+
if (batch.registrations.length === 0) continue;
|
|
656
|
+
this.#forwardNoRelay(session, server, {
|
|
657
|
+
jsonrpc: "2.0",
|
|
658
|
+
id: "replaced",
|
|
659
|
+
method: "client/registerCapability",
|
|
660
|
+
params: cloneParams(batch),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
for (const params of server.diagnostics.values()) this.#sendDiagnostics(session, server, params);
|
|
664
|
+
for (const params of server.progress.values()) {
|
|
665
|
+
this.#sendSession(session, { jsonrpc: "2.0", method: "$/progress", params: cloneParams(params) });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#sendSession(session: Session, message: RpcMessage): void {
|
|
670
|
+
if (!session.closed && !session.socket.destroyed) session.socket.write(frame(message));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
#writeServer(server: ServerInstance, message: RpcMessage): Promise<void> {
|
|
674
|
+
const write = server.writeQueue
|
|
675
|
+
.catch(() => {})
|
|
676
|
+
.then(async () => {
|
|
677
|
+
const data = frame(message);
|
|
678
|
+
const pendingWrite = Promise.resolve(server.proc.stdin.write(data));
|
|
679
|
+
void pendingWrite.catch(() => {});
|
|
680
|
+
await Promise.all([pendingWrite, Promise.resolve(server.proc.stdin.flush())]);
|
|
681
|
+
});
|
|
682
|
+
server.writeQueue = write.catch(error => {
|
|
683
|
+
logger.warn("LSP mux server write failed", { server: server.key, error: String(error) });
|
|
684
|
+
});
|
|
685
|
+
return write;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async #closeSession(session: Session): Promise<void> {
|
|
689
|
+
if (session.closed) return;
|
|
690
|
+
session.closed = true;
|
|
691
|
+
this.#sessions.delete(session);
|
|
692
|
+
const server = session.server;
|
|
693
|
+
if (server) {
|
|
694
|
+
server.sessions.delete(session);
|
|
695
|
+
for (const uri of session.openUris) {
|
|
696
|
+
const record = server.documents.get(uri);
|
|
697
|
+
if (!record) continue;
|
|
698
|
+
record.perSession.delete(session);
|
|
699
|
+
if (record.perSession.size === 0) {
|
|
700
|
+
server.documents.delete(uri);
|
|
701
|
+
await this.#writeServer(server, {
|
|
702
|
+
jsonrpc: "2.0",
|
|
703
|
+
method: "textDocument/didClose",
|
|
704
|
+
params: { textDocument: { uri } },
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
for (const [muxId, pending] of server.pending) {
|
|
709
|
+
if (pending.session !== session) continue;
|
|
710
|
+
pending.drop = true;
|
|
711
|
+
await this.#writeServer(server, { jsonrpc: "2.0", method: "$/cancelRequest", params: { id: muxId } });
|
|
712
|
+
}
|
|
713
|
+
server.initializeWaiters.delete(session);
|
|
714
|
+
if (server.sessions.size === 0 && !server.stopping) {
|
|
715
|
+
server.lingerTimer = setTimeout(() => {
|
|
716
|
+
if (server.sessions.size === 0) void this.#stopServer(server);
|
|
717
|
+
}, SERVER_LINGER_MS);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (this.#sessions.size === 0) this.#armMuxIdle();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
#serverExited(server: ServerInstance): void {
|
|
724
|
+
server.stopping = true;
|
|
725
|
+
if (this.#servers.get(server.key) === server) this.#servers.delete(server.key);
|
|
726
|
+
if (server.lingerTimer) clearTimeout(server.lingerTimer);
|
|
727
|
+
server.pending.clear();
|
|
728
|
+
for (const session of [...server.sessions]) session.socket.destroy();
|
|
729
|
+
server.sessions.clear();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async #stopServer(server: ServerInstance): Promise<void> {
|
|
733
|
+
if (server.stopping) return;
|
|
734
|
+
server.stopping = true;
|
|
735
|
+
if (server.lingerTimer) clearTimeout(server.lingerTimer);
|
|
736
|
+
const id = server.muxId();
|
|
737
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
738
|
+
server.pending.set(id, { resolveInternal: resolve });
|
|
739
|
+
try {
|
|
740
|
+
await this.#writeServer(server, { jsonrpc: "2.0", id, method: "shutdown", params: null });
|
|
741
|
+
await Promise.race([promise, Bun.sleep(SHUTDOWN_BUDGET_MS)]);
|
|
742
|
+
await this.#writeServer(server, { jsonrpc: "2.0", method: "exit" });
|
|
743
|
+
} catch (error) {
|
|
744
|
+
logger.warn("LSP mux graceful server shutdown failed", { server: server.key, error: String(error) });
|
|
745
|
+
} finally {
|
|
746
|
+
this.#killServer(server);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
#killServer(server: ServerInstance): void {
|
|
751
|
+
server.stopping = true;
|
|
752
|
+
try {
|
|
753
|
+
server.proc.kill();
|
|
754
|
+
} catch {
|
|
755
|
+
this.#serverExited(server);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#disarmMuxIdle(): void {
|
|
760
|
+
if (this.#idleTimer) clearTimeout(this.#idleTimer);
|
|
761
|
+
this.#idleTimer = undefined;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
#armMuxIdle(): void {
|
|
765
|
+
this.#disarmMuxIdle();
|
|
766
|
+
if (this.#shuttingDown || this.#sessions.size > 0) return;
|
|
767
|
+
this.#idleTimer = setTimeout(() => {
|
|
768
|
+
if (this.#sessions.size === 0) this.onIdle?.();
|
|
769
|
+
}, MUX_IDLE_MS);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Start the detached LSP mux selected by the CLI worker host environment. */
|
|
774
|
+
export async function startLspMuxFromEnvironment(): Promise<void> {
|
|
775
|
+
const endpoint = process.env[LSP_MUX_SOCKET_ENV];
|
|
776
|
+
const projectDir = process.env[LSP_MUX_PROJECT_DIR_ENV];
|
|
777
|
+
if (!endpoint || !projectDir) throw new Error("LSP mux environment is incomplete");
|
|
778
|
+
delete process.env[LSP_MUX_SOCKET_ENV];
|
|
779
|
+
delete process.env[LSP_MUX_PROJECT_DIR_ENV];
|
|
780
|
+
setProcessName("omp lsp mux");
|
|
781
|
+
const server = new LspMuxServer();
|
|
782
|
+
const stopped = Promise.withResolvers<void>();
|
|
783
|
+
server.onIdle = () => {
|
|
784
|
+
void server.shutdown().finally(() => {
|
|
785
|
+
stopped.resolve();
|
|
786
|
+
process.exit(0);
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
const cancelCleanup = postmortem.register("lsp-mux", () => server.shutdown());
|
|
790
|
+
try {
|
|
791
|
+
await server.listen(endpoint);
|
|
792
|
+
process.stdout.write(`${lspMuxReadyBanner(endpoint)}\n`);
|
|
793
|
+
await stopped.promise;
|
|
794
|
+
} finally {
|
|
795
|
+
cancelCleanup();
|
|
796
|
+
}
|
|
797
|
+
}
|