@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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quiet daemon-broker ensure helpers shared by broker-owned singleton daemons
|
|
3
|
+
* (shared Chromium, browser relay, LSP mux). Each treats broker/daemon errors
|
|
4
|
+
* as "absent" so ensure loops can retry or adopt a cross-process race winner
|
|
5
|
+
* instead of failing the caller.
|
|
6
|
+
*/
|
|
7
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { throwIfAborted } from "../tools/tool-errors";
|
|
9
|
+
import type { DaemonBrokerClient } from "./client";
|
|
10
|
+
import type { DaemonSnapshot } from "./protocol";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_READY_TIMEOUT_MS = 30_000;
|
|
13
|
+
const STOP_TIMEOUT_MS = 5_000;
|
|
14
|
+
|
|
15
|
+
/** Snapshot a broker daemon, treating "unknown daemon" and broker errors as absent. */
|
|
16
|
+
export async function describeQuietly(
|
|
17
|
+
client: DaemonBrokerClient,
|
|
18
|
+
name: string,
|
|
19
|
+
label: string,
|
|
20
|
+
signal?: AbortSignal,
|
|
21
|
+
): Promise<DaemonSnapshot | undefined> {
|
|
22
|
+
try {
|
|
23
|
+
const result = await client.request({ op: "describe", name }, signal);
|
|
24
|
+
return result.op === "describe" ? result.daemon : undefined;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throwIfAborted(signal);
|
|
27
|
+
logger.debug(`${label} describe failed`, {
|
|
28
|
+
name,
|
|
29
|
+
error: error instanceof Error ? error.message : String(error),
|
|
30
|
+
});
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Block until the daemon reports ready; undefined on timeout or pre-ready exit. */
|
|
36
|
+
export async function waitReady(
|
|
37
|
+
client: DaemonBrokerClient,
|
|
38
|
+
name: string,
|
|
39
|
+
label: string,
|
|
40
|
+
signal?: AbortSignal,
|
|
41
|
+
timeoutMs: number = DEFAULT_READY_TIMEOUT_MS,
|
|
42
|
+
): Promise<DaemonSnapshot | undefined> {
|
|
43
|
+
try {
|
|
44
|
+
const result = await client.request({ op: "wait", name, for: "ready", timeoutMs }, signal);
|
|
45
|
+
if (result.op !== "wait" || result.timedOut) return undefined;
|
|
46
|
+
return result.daemon;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throwIfAborted(signal);
|
|
49
|
+
logger.debug(`${label} ready wait failed`, {
|
|
50
|
+
name,
|
|
51
|
+
error: error instanceof Error ? error.message : String(error),
|
|
52
|
+
});
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Best-effort stop before replacing a wedged or endpoint-less daemon. */
|
|
58
|
+
export async function stopQuietly(
|
|
59
|
+
client: DaemonBrokerClient,
|
|
60
|
+
name: string,
|
|
61
|
+
label: string,
|
|
62
|
+
signal?: AbortSignal,
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
await client.request({ op: "stop", name, timeoutMs: STOP_TIMEOUT_MS }, signal);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throwIfAborted(signal);
|
|
68
|
+
logger.debug(`${label} stop failed`, {
|
|
69
|
+
name,
|
|
70
|
+
error: error instanceof Error ? error.message : String(error),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/launch/paths.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { getDaemonRuntimeDir } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
/** Resolve the private runtime directory shared by omp processes in one project directory. */
|
|
5
5
|
export { getDaemonRuntimeDir as daemonRuntimeDir };
|
|
6
6
|
|
|
7
|
-
/** Resolve the Unix socket or Windows named pipe used by one
|
|
7
|
+
/** Resolve the Unix socket or Windows named pipe used by one daemon broker scope. */
|
|
8
8
|
export function daemonBrokerEndpoint(projectDir: string, runtimeDir: string): string {
|
|
9
9
|
if (process.platform === "win32") {
|
|
10
10
|
const key = Bun.hash.wyhash(path.resolve(projectDir)).toString(16).padStart(16, "0");
|
package/src/launch/protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@ export const DAEMON_BROKER_WORKER_ARG = "__omp_worker_daemon_broker";
|
|
|
8
8
|
export const DAEMON_PTY_COLUMNS = 120;
|
|
9
9
|
export const DAEMON_PTY_ROWS = 40;
|
|
10
10
|
|
|
11
|
-
/** Environment key carrying the broker's canonical project directory. */
|
|
11
|
+
/** Environment key carrying the broker's canonical project or synthetic global scope directory. */
|
|
12
12
|
export const DAEMON_PROJECT_DIR_ENV = "OMP_DAEMON_PROJECT_DIR";
|
|
13
13
|
|
|
14
14
|
/** Environment key carrying the broker's private runtime directory. */
|
|
@@ -45,7 +45,7 @@ export interface DaemonSpec {
|
|
|
45
45
|
detached: boolean;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/** Serializable daemon state visible to every client in one
|
|
48
|
+
/** Serializable daemon state visible to every client in one broker scope. */
|
|
49
49
|
export interface DaemonSnapshot {
|
|
50
50
|
name: string;
|
|
51
51
|
id: string;
|
|
@@ -122,12 +122,29 @@ export type DaemonRpcResult =
|
|
|
122
122
|
export interface DaemonWireRequest {
|
|
123
123
|
id: string;
|
|
124
124
|
token: string;
|
|
125
|
+
owners?: string[];
|
|
126
|
+
detachedOwners?: string[];
|
|
127
|
+
completionEvents?: boolean;
|
|
128
|
+
completionAcks?: string[];
|
|
129
|
+
completionUnsubscribes?: string[];
|
|
130
|
+
completionReplays?: string[];
|
|
131
|
+
completionSubscriptionId?: string;
|
|
125
132
|
operation: DaemonOperation;
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
/** Response envelope kept raw until matched with its pending operation. */
|
|
129
136
|
export type DaemonWireResponse = { id: string; ok: true; result: unknown } | { id: string; ok: false; error: string };
|
|
130
137
|
|
|
138
|
+
/** Unsolicited terminal completion sent to the socket that owns a daemon. */
|
|
139
|
+
export interface DaemonCompletionNotification {
|
|
140
|
+
event: "daemon-completed";
|
|
141
|
+
completionId: string;
|
|
142
|
+
owner: string;
|
|
143
|
+
daemon: DaemonSnapshot;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type DaemonWireMessage = DaemonWireResponse | DaemonCompletionNotification;
|
|
147
|
+
|
|
131
148
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
132
149
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
150
|
}
|
|
@@ -268,6 +285,27 @@ export function parseDaemonWireRequest(value: unknown): DaemonWireRequest {
|
|
|
268
285
|
return {
|
|
269
286
|
id: stringValue(source.id, "request.id"),
|
|
270
287
|
token: stringValue(source.token, "request.token"),
|
|
288
|
+
owners: source.owners === undefined ? undefined : stringArray(source.owners, "request.owners"),
|
|
289
|
+
detachedOwners:
|
|
290
|
+
source.detachedOwners === undefined ? undefined : stringArray(source.detachedOwners, "request.detachedOwners"),
|
|
291
|
+
completionEvents:
|
|
292
|
+
source.completionEvents === undefined
|
|
293
|
+
? undefined
|
|
294
|
+
: booleanValue(source.completionEvents, "request.completionEvents"),
|
|
295
|
+
completionAcks:
|
|
296
|
+
source.completionAcks === undefined ? undefined : stringArray(source.completionAcks, "request.completionAcks"),
|
|
297
|
+
completionUnsubscribes:
|
|
298
|
+
source.completionUnsubscribes === undefined
|
|
299
|
+
? undefined
|
|
300
|
+
: stringArray(source.completionUnsubscribes, "request.completionUnsubscribes"),
|
|
301
|
+
completionReplays:
|
|
302
|
+
source.completionReplays === undefined
|
|
303
|
+
? undefined
|
|
304
|
+
: stringArray(source.completionReplays, "request.completionReplays"),
|
|
305
|
+
completionSubscriptionId:
|
|
306
|
+
source.completionSubscriptionId === undefined
|
|
307
|
+
? undefined
|
|
308
|
+
: stringValue(source.completionSubscriptionId, "request.completionSubscriptionId"),
|
|
271
309
|
operation: parseDaemonOperation(source.operation),
|
|
272
310
|
};
|
|
273
311
|
}
|
|
@@ -281,6 +319,20 @@ export function parseDaemonWireResponse(value: unknown): DaemonWireResponse {
|
|
|
281
319
|
throw new Error("response.ok must be a boolean");
|
|
282
320
|
}
|
|
283
321
|
|
|
322
|
+
/** Decode one broker response or unsolicited completion notification. */
|
|
323
|
+
export function parseDaemonWireMessage(value: unknown): DaemonWireMessage {
|
|
324
|
+
const source = record(value, "daemon message");
|
|
325
|
+
if (source.event === "daemon-completed") {
|
|
326
|
+
return {
|
|
327
|
+
event: "daemon-completed",
|
|
328
|
+
completionId: stringValue(source.completionId, "completion.id"),
|
|
329
|
+
owner: stringValue(source.owner, "completion.owner"),
|
|
330
|
+
daemon: parseDaemonSnapshot(source.daemon),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return parseDaemonWireResponse(value);
|
|
334
|
+
}
|
|
335
|
+
|
|
284
336
|
function parseDaemonOperation(value: unknown): DaemonOperation {
|
|
285
337
|
const source = record(value, "daemon operation");
|
|
286
338
|
const op = stringValue(source.op, "operation.op");
|
package/src/lsp/client.ts
CHANGED
|
@@ -4,12 +4,15 @@ import { MessageFramer } from "../jsonrpc/message-framing";
|
|
|
4
4
|
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
5
5
|
import { applyWorkspaceEdit } from "./edits";
|
|
6
6
|
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
|
|
7
|
+
import { connectSharedLspTransport } from "./mux/daemon";
|
|
7
8
|
import type {
|
|
8
9
|
LspClient,
|
|
9
10
|
LspJsonRpcId,
|
|
10
11
|
LspJsonRpcNotification,
|
|
11
12
|
LspJsonRpcRequest,
|
|
12
13
|
LspJsonRpcResponse,
|
|
14
|
+
LspTransport,
|
|
15
|
+
LspWriteSink,
|
|
13
16
|
PublishDiagnosticsParams,
|
|
14
17
|
ServerConfig,
|
|
15
18
|
WorkspaceEdit,
|
|
@@ -34,6 +37,17 @@ let idleTimeoutMs: number | null = null;
|
|
|
34
37
|
let idleCheckInterval: NodeJS.Timeout | null = null;
|
|
35
38
|
const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
|
|
36
39
|
|
|
40
|
+
// Broker-shared server mode (one language server per project shared by every
|
|
41
|
+
// omp instance through the LSP mux daemon). Off by default so embedders and
|
|
42
|
+
// tests that drive getOrCreateClient directly never touch the daemon broker;
|
|
43
|
+
// the SDK turns it on from the `lsp.shared` setting at session creation.
|
|
44
|
+
let sharedLspEnabled = false;
|
|
45
|
+
|
|
46
|
+
/** Enable or disable attaching to broker-shared language servers. */
|
|
47
|
+
export function setSharedLspEnabled(enabled: boolean): void {
|
|
48
|
+
sharedLspEnabled = enabled;
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
/**
|
|
38
52
|
* Configure the idle timeout for LSP clients.
|
|
39
53
|
* @param ms - Timeout in milliseconds, or null/undefined to disable
|
|
@@ -209,7 +223,7 @@ class LspDrainAbortError extends Error {
|
|
|
209
223
|
}
|
|
210
224
|
|
|
211
225
|
async function writeMessage(
|
|
212
|
-
sink:
|
|
226
|
+
sink: LspWriteSink,
|
|
213
227
|
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
214
228
|
signal?: AbortSignal,
|
|
215
229
|
): Promise<void> {
|
|
@@ -742,7 +756,14 @@ export async function getOrCreateClient(
|
|
|
742
756
|
? await getLspmuxCommand(baseCommand, baseArgs)
|
|
743
757
|
: { command: baseCommand, args: baseArgs };
|
|
744
758
|
|
|
745
|
-
|
|
759
|
+
// Prefer the broker-shared server unless an external lspmux wrapper is
|
|
760
|
+
// already multiplexing this command. Any shared-path failure falls back
|
|
761
|
+
// to a private spawn so LSP never regresses on broker trouble.
|
|
762
|
+
let proc: LspTransport | null = null;
|
|
763
|
+
if (sharedLspEnabled && command === baseCommand) {
|
|
764
|
+
proc = await connectSharedLspTransport({ command, args, cwd, env, signal });
|
|
765
|
+
}
|
|
766
|
+
proc ??= ptree.spawn([command, ...args], {
|
|
746
767
|
cwd,
|
|
747
768
|
stdin: "pipe",
|
|
748
769
|
env: env ? { ...Bun.env, ...env } : undefined,
|
package/src/lsp/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
} from "./edits";
|
|
48
48
|
import { resolveFormatOptions } from "./format-options";
|
|
49
49
|
import { detectLspmux } from "./lspmux";
|
|
50
|
+
import { MUX_RESTART_METHOD } from "./mux/protocol";
|
|
50
51
|
import {
|
|
51
52
|
type CodeAction,
|
|
52
53
|
type CodeActionContext,
|
|
@@ -535,6 +536,13 @@ async function reloadServer(client: LspClient, serverName: string, signal?: Abor
|
|
|
535
536
|
// from the registry by identity and awaiting confirmed process exit) so
|
|
536
537
|
// the next request cold-starts a fresh client. A kill that never confirms
|
|
537
538
|
// exit is not a restart: surface the teardown failure truthfully.
|
|
539
|
+
//
|
|
540
|
+
// On a broker-shared link a per-session teardown only detaches this
|
|
541
|
+
// process while the wedged server keeps serving everyone else — ask the
|
|
542
|
+
// mux to kill the shared server first (best-effort; it also severs us).
|
|
543
|
+
if (client.proc.sharedMux) {
|
|
544
|
+
await sendNotification(client, MUX_RESTART_METHOD, undefined, AbortSignal.timeout(2_000)).catch(() => {});
|
|
545
|
+
}
|
|
538
546
|
if (!(await shutdownClientInstance(client))) {
|
|
539
547
|
throw new Error(`Failed to restart ${serverName}: server process did not exit after kill`);
|
|
540
548
|
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client half of the broker-shared LSP mux.
|
|
3
|
+
*
|
|
4
|
+
* `connectSharedLspTransport` ensures the per-project mux daemon is running
|
|
5
|
+
* under the daemon broker (same lifecycle as the shared Chromium: started on
|
|
6
|
+
* first use, stopped when the last omp process in the project exits), dials
|
|
7
|
+
* its socket, performs the `omp/muxConnect` handshake, and returns an
|
|
8
|
+
* {@link LspTransport} the ordinary LSP client machinery drives exactly like
|
|
9
|
+
* a locally spawned server. Every failure degrades to `null` so callers fall
|
|
10
|
+
* back to a process-local spawn.
|
|
11
|
+
*/
|
|
12
|
+
import * as net from "node:net";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { logger, ptree } from "@oh-my-pi/pi-utils";
|
|
16
|
+
import { MessageFramer } from "../../jsonrpc/message-framing";
|
|
17
|
+
import { daemonClientForProject } from "../../launch/client";
|
|
18
|
+
import { describeQuietly, stopQuietly, waitReady } from "../../launch/ensure";
|
|
19
|
+
import { daemonRuntimeDir } from "../../launch/paths";
|
|
20
|
+
import { resolveWorkerSpawnCmd, SMOKE_TEST_TIMEOUT_MS, workerEnvFromParent } from "../../subprocess/worker-client";
|
|
21
|
+
import type { LspJsonRpcRequest, LspJsonRpcResponse, LspTransport, LspWriteSink } from "../types";
|
|
22
|
+
import {
|
|
23
|
+
LSP_MUX_DAEMON_NAME,
|
|
24
|
+
LSP_MUX_PROJECT_DIR_ENV,
|
|
25
|
+
LSP_MUX_READY_PATTERN,
|
|
26
|
+
LSP_MUX_SOCKET_ENV,
|
|
27
|
+
LSP_MUX_WORKER_ARG,
|
|
28
|
+
lspMuxEndpoint,
|
|
29
|
+
MUX_CONNECT_METHOD,
|
|
30
|
+
MUX_PING_METHOD,
|
|
31
|
+
MUX_PING_RESULT,
|
|
32
|
+
type MuxConnectParams,
|
|
33
|
+
type MuxConnectResult,
|
|
34
|
+
} from "./protocol";
|
|
35
|
+
|
|
36
|
+
const CONNECT_TIMEOUT_MS = 3_000;
|
|
37
|
+
const HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
38
|
+
const PROBE_TIMEOUT_MS = 1_500;
|
|
39
|
+
const READY_TIMEOUT_MS = 15_000;
|
|
40
|
+
/** probe→describe→start rounds; bounds cross-process start races and wedged-mux replacement. */
|
|
41
|
+
const ENSURE_ATTEMPTS = 3;
|
|
42
|
+
|
|
43
|
+
/** Dial a mux endpoint (Unix socket or Windows named pipe) with a bounded connect. */
|
|
44
|
+
function connectEndpoint(endpoint: string, timeoutMs: number): Promise<net.Socket> {
|
|
45
|
+
const { promise, resolve, reject } = Promise.withResolvers<net.Socket>();
|
|
46
|
+
const socket = net.connect(endpoint);
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
socket.destroy();
|
|
49
|
+
reject(new Error(`Timed out connecting to LSP mux at ${endpoint}`));
|
|
50
|
+
}, timeoutMs);
|
|
51
|
+
socket.once("connect", () => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
socket.removeListener("error", onError);
|
|
54
|
+
resolve(socket);
|
|
55
|
+
});
|
|
56
|
+
const onError = (error: Error) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
reject(error);
|
|
59
|
+
};
|
|
60
|
+
socket.once("error", onError);
|
|
61
|
+
return promise;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Send one request on a fresh socket and await its response. Used only
|
|
66
|
+
* before the transport takes over the socket; leftover bytes after the
|
|
67
|
+
* response are returned so the transport's stream can replay them.
|
|
68
|
+
*/
|
|
69
|
+
function requestOnSocket(
|
|
70
|
+
socket: net.Socket,
|
|
71
|
+
request: LspJsonRpcRequest,
|
|
72
|
+
timeoutMs: number,
|
|
73
|
+
): Promise<{ response: LspJsonRpcResponse; leftover: Buffer }> {
|
|
74
|
+
const { promise, resolve, reject } = Promise.withResolvers<{ response: LspJsonRpcResponse; leftover: Buffer }>();
|
|
75
|
+
const framer = new MessageFramer(Buffer.alloc(0));
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
cleanup();
|
|
78
|
+
reject(new Error(`LSP mux ${request.method} timed out`));
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
const onData = (chunk: Buffer) => {
|
|
81
|
+
framer.push(chunk);
|
|
82
|
+
for (const text of framer.drain(() => {})) {
|
|
83
|
+
let message: LspJsonRpcResponse;
|
|
84
|
+
try {
|
|
85
|
+
message = JSON.parse(text);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
cleanup();
|
|
88
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (message.id !== request.id) continue;
|
|
92
|
+
cleanup();
|
|
93
|
+
if (message.error) reject(new Error(`LSP mux ${request.method} failed: ${message.error.message}`));
|
|
94
|
+
else resolve({ response: message, leftover: framer.remainder() });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const onClose = () => {
|
|
99
|
+
cleanup();
|
|
100
|
+
reject(new Error("LSP mux connection closed during handshake"));
|
|
101
|
+
};
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
socket.removeListener("data", onData);
|
|
105
|
+
socket.removeListener("close", onClose);
|
|
106
|
+
socket.removeListener("error", onClose);
|
|
107
|
+
};
|
|
108
|
+
socket.on("data", onData);
|
|
109
|
+
socket.once("close", onClose);
|
|
110
|
+
socket.once("error", onClose);
|
|
111
|
+
const content = JSON.stringify(request);
|
|
112
|
+
socket.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n${content}`);
|
|
113
|
+
return promise;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wrap a connected, handshaken mux socket as an {@link LspTransport}.
|
|
118
|
+
* Socket close maps to a clean "process exit" (code 0) so the LSP client's
|
|
119
|
+
* crash-recovery path treats a dead mux exactly like a dead server.
|
|
120
|
+
*/
|
|
121
|
+
function socketTransport(socket: net.Socket, leftover: Buffer, pid: number | undefined): LspTransport {
|
|
122
|
+
const exited = Promise.withResolvers<number>();
|
|
123
|
+
let exitCode: number | null = null;
|
|
124
|
+
let needDrain = false;
|
|
125
|
+
socket.on("error", () => {
|
|
126
|
+
// "close" always follows; swallowing here prevents an unhandled error event.
|
|
127
|
+
});
|
|
128
|
+
socket.once("close", () => {
|
|
129
|
+
exitCode = 0;
|
|
130
|
+
exited.resolve(0);
|
|
131
|
+
});
|
|
132
|
+
const stdout = new ReadableStream<Uint8Array>({
|
|
133
|
+
start(controller) {
|
|
134
|
+
if (leftover.byteLength > 0) controller.enqueue(new Uint8Array(leftover));
|
|
135
|
+
socket.on("data", (chunk: Buffer) =>
|
|
136
|
+
controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)),
|
|
137
|
+
);
|
|
138
|
+
socket.once("close", () => {
|
|
139
|
+
try {
|
|
140
|
+
controller.close();
|
|
141
|
+
} catch {
|
|
142
|
+
// already errored/closed
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const stdin: LspWriteSink = {
|
|
148
|
+
write(data) {
|
|
149
|
+
needDrain = !socket.write(data);
|
|
150
|
+
return typeof data === "string" ? Buffer.byteLength(data, "utf-8") : data.byteLength;
|
|
151
|
+
},
|
|
152
|
+
flush() {
|
|
153
|
+
if (!needDrain || socket.destroyed) return;
|
|
154
|
+
return new Promise<void>(resolveDrain => {
|
|
155
|
+
const onDrain = () => {
|
|
156
|
+
needDrain = false;
|
|
157
|
+
socket.removeListener("close", onDrain);
|
|
158
|
+
resolveDrain();
|
|
159
|
+
};
|
|
160
|
+
socket.once("drain", onDrain);
|
|
161
|
+
socket.once("close", onDrain);
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
stdin,
|
|
167
|
+
stdout,
|
|
168
|
+
exited: exited.promise,
|
|
169
|
+
get exitCode() {
|
|
170
|
+
return exitCode;
|
|
171
|
+
},
|
|
172
|
+
pid,
|
|
173
|
+
sharedMux: true,
|
|
174
|
+
kill() {
|
|
175
|
+
socket.destroy();
|
|
176
|
+
},
|
|
177
|
+
peekStderr() {
|
|
178
|
+
return "";
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let nextHandshakeId = 1;
|
|
184
|
+
|
|
185
|
+
/** Dial and handshake one server link; throws on any failure. */
|
|
186
|
+
async function dialMuxServer(endpoint: string, params: MuxConnectParams): Promise<LspTransport> {
|
|
187
|
+
const socket = await connectEndpoint(endpoint, CONNECT_TIMEOUT_MS);
|
|
188
|
+
socket.setNoDelay(true);
|
|
189
|
+
try {
|
|
190
|
+
const { response, leftover } = await requestOnSocket(
|
|
191
|
+
socket,
|
|
192
|
+
{ jsonrpc: "2.0", id: `connect:${nextHandshakeId++}`, method: MUX_CONNECT_METHOD, params },
|
|
193
|
+
HANDSHAKE_TIMEOUT_MS,
|
|
194
|
+
);
|
|
195
|
+
const result = response.result as MuxConnectResult;
|
|
196
|
+
return socketTransport(socket, leftover, result.pid);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
socket.destroy();
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** True when a mux answers the ping handshake at `endpoint`. */
|
|
204
|
+
async function probeMux(endpoint: string): Promise<boolean> {
|
|
205
|
+
try {
|
|
206
|
+
const socket = await connectEndpoint(endpoint, PROBE_TIMEOUT_MS);
|
|
207
|
+
try {
|
|
208
|
+
const { response } = await requestOnSocket(
|
|
209
|
+
socket,
|
|
210
|
+
{ jsonrpc: "2.0", id: "probe", method: MUX_PING_METHOD, params: null },
|
|
211
|
+
PROBE_TIMEOUT_MS,
|
|
212
|
+
);
|
|
213
|
+
return response.result === MUX_PING_RESULT;
|
|
214
|
+
} finally {
|
|
215
|
+
socket.destroy();
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Ensure the project's mux daemon is running under the broker and reachable.
|
|
224
|
+
* Returns its endpoint, or null when the shared path is unavailable.
|
|
225
|
+
*/
|
|
226
|
+
async function ensureLspMuxDaemon(projectDir: string, signal?: AbortSignal): Promise<string | null> {
|
|
227
|
+
const client = await daemonClientForProject(projectDir);
|
|
228
|
+
const endpoint = lspMuxEndpoint(client.projectDir, daemonRuntimeDir(client.projectDir));
|
|
229
|
+
// The broker connection doubles as the presence lease keeping the daemon alive.
|
|
230
|
+
await client.request({ op: "ping" }, signal);
|
|
231
|
+
if (await probeMux(endpoint)) return endpoint;
|
|
232
|
+
const spawn = resolveWorkerSpawnCmd(LSP_MUX_WORKER_ARG);
|
|
233
|
+
for (let attempt = 0; attempt < ENSURE_ATTEMPTS; attempt++) {
|
|
234
|
+
signal?.throwIfAborted();
|
|
235
|
+
// A concurrent start may have won since the last round; adopt it.
|
|
236
|
+
if (await probeMux(endpoint)) return endpoint;
|
|
237
|
+
const existing = await describeQuietly(client, LSP_MUX_DAEMON_NAME, "LSP mux", signal);
|
|
238
|
+
if (existing && existing.state !== "exited" && existing.state !== "failed") {
|
|
239
|
+
if (existing.readyAt === undefined) {
|
|
240
|
+
await waitReady(client, LSP_MUX_DAEMON_NAME, "LSP mux", signal, READY_TIMEOUT_MS);
|
|
241
|
+
}
|
|
242
|
+
if (await probeMux(endpoint)) return endpoint;
|
|
243
|
+
// Live record but nothing listening: replace the wedged daemon.
|
|
244
|
+
await stopQuietly(client, LSP_MUX_DAEMON_NAME, "LSP mux", signal);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const started = await client.request(
|
|
249
|
+
{
|
|
250
|
+
op: "start",
|
|
251
|
+
spec: {
|
|
252
|
+
name: LSP_MUX_DAEMON_NAME,
|
|
253
|
+
application: spawn.cmd[0]!,
|
|
254
|
+
args: spawn.cmd.slice(1),
|
|
255
|
+
env: {
|
|
256
|
+
[LSP_MUX_SOCKET_ENV]: endpoint,
|
|
257
|
+
[LSP_MUX_PROJECT_DIR_ENV]: client.projectDir,
|
|
258
|
+
},
|
|
259
|
+
cwd: spawn.cwd ?? client.projectDir,
|
|
260
|
+
pty: false,
|
|
261
|
+
ready: { log: LSP_MUX_READY_PATTERN, timeoutMs: READY_TIMEOUT_MS },
|
|
262
|
+
restart: "no",
|
|
263
|
+
persist: false,
|
|
264
|
+
detached: false,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
signal,
|
|
268
|
+
);
|
|
269
|
+
if (started.op !== "start") continue;
|
|
270
|
+
if (await probeMux(endpoint)) return endpoint;
|
|
271
|
+
await stopQuietly(client, LSP_MUX_DAEMON_NAME, "LSP mux", signal);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
signal?.throwIfAborted();
|
|
274
|
+
// Lost a cross-process start race; the next round adopts the winner.
|
|
275
|
+
logger.debug("LSP mux start contention", {
|
|
276
|
+
name: LSP_MUX_DAEMON_NAME,
|
|
277
|
+
error: error instanceof Error ? error.message : String(error),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Open a broker-shared transport for one language server, ensuring the mux
|
|
286
|
+
* daemon first. Returns null (after a debug log) when the shared path is
|
|
287
|
+
* unavailable so the caller falls back to a process-local spawn.
|
|
288
|
+
*/
|
|
289
|
+
export async function connectSharedLspTransport(opts: {
|
|
290
|
+
command: string;
|
|
291
|
+
args: string[];
|
|
292
|
+
cwd: string;
|
|
293
|
+
env?: Record<string, string>;
|
|
294
|
+
signal?: AbortSignal;
|
|
295
|
+
}): Promise<LspTransport | null> {
|
|
296
|
+
try {
|
|
297
|
+
const endpoint = await ensureLspMuxDaemon(opts.cwd, opts.signal);
|
|
298
|
+
if (!endpoint) return null;
|
|
299
|
+
return await dialMuxServer(endpoint, {
|
|
300
|
+
command: opts.command,
|
|
301
|
+
args: opts.args,
|
|
302
|
+
cwd: opts.cwd,
|
|
303
|
+
env: opts.env,
|
|
304
|
+
});
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (opts.signal?.aborted) throw error;
|
|
307
|
+
logger.debug("Shared LSP transport unavailable; falling back to local spawn", {
|
|
308
|
+
command: opts.command,
|
|
309
|
+
cwd: opts.cwd,
|
|
310
|
+
error: error instanceof Error ? error.message : String(error),
|
|
311
|
+
});
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Exercise worker-host mux startup and the ping handshake for distribution smoke tests. */
|
|
317
|
+
export async function smokeTestLspMux(): Promise<void> {
|
|
318
|
+
const endpoint =
|
|
319
|
+
process.platform === "win32"
|
|
320
|
+
? `\\\\.\\pipe\\omp-lsp-mux-smoke-${process.pid.toString(16)}`
|
|
321
|
+
: path.join(os.tmpdir(), `omp-lsp-mux-smoke-${process.pid.toString(36)}.sock`);
|
|
322
|
+
const spawn = resolveWorkerSpawnCmd(LSP_MUX_WORKER_ARG);
|
|
323
|
+
const proc = ptree.spawn(spawn.cmd, {
|
|
324
|
+
cwd: spawn.cwd,
|
|
325
|
+
env: workerEnvFromParent({
|
|
326
|
+
[LSP_MUX_SOCKET_ENV]: endpoint,
|
|
327
|
+
[LSP_MUX_PROJECT_DIR_ENV]: process.cwd(),
|
|
328
|
+
}),
|
|
329
|
+
});
|
|
330
|
+
try {
|
|
331
|
+
const deadline = Date.now() + SMOKE_TEST_TIMEOUT_MS;
|
|
332
|
+
let alive = false;
|
|
333
|
+
while (Date.now() < deadline) {
|
|
334
|
+
if (proc.exitCode !== null) break;
|
|
335
|
+
if (await probeMux(endpoint)) {
|
|
336
|
+
alive = true;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
await Bun.sleep(200);
|
|
340
|
+
}
|
|
341
|
+
if (!alive) {
|
|
342
|
+
throw new Error(`lsp mux smoke failed: no ping response (${proc.peekStderr().slice(-500) || "no stderr"})`);
|
|
343
|
+
}
|
|
344
|
+
} finally {
|
|
345
|
+
proc.kill();
|
|
346
|
+
await proc.exited.catch(() => {});
|
|
347
|
+
}
|
|
348
|
+
}
|