@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.
Files changed (348) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
  3. package/dist/cli.js +4438 -3962
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/cli/args.d.ts +4 -1
  6. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  7. package/dist/types/cli/command-help.d.ts +99 -0
  8. package/dist/types/cli/gc-cli.d.ts +1 -0
  9. package/dist/types/cli/help-extra.d.ts +2 -0
  10. package/dist/types/cli/thinking-levels.d.ts +5 -0
  11. package/dist/types/cli/update-cli.d.ts +13 -0
  12. package/dist/types/commands/browser-relay.d.ts +38 -0
  13. package/dist/types/commands/commit.d.ts +3 -0
  14. package/dist/types/commands/complete.d.ts +1 -1
  15. package/dist/types/commands/completions.d.ts +6 -0
  16. package/dist/types/commands/grep.d.ts +3 -0
  17. package/dist/types/commands/join.d.ts +4 -0
  18. package/dist/types/commands/launch-help.d.ts +171 -0
  19. package/dist/types/commands/launch.d.ts +5 -1
  20. package/dist/types/commands/models.d.ts +3 -0
  21. package/dist/types/commands/say.d.ts +10 -0
  22. package/dist/types/commands/worktree.d.ts +3 -0
  23. package/dist/types/config/keybindings.d.ts +5 -0
  24. package/dist/types/config/model-discovery.d.ts +2 -2
  25. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  26. package/dist/types/config/models-config-schema.d.ts +2 -0
  27. package/dist/types/config/models-config.d.ts +1 -0
  28. package/dist/types/config/service-tier.d.ts +2 -0
  29. package/dist/types/config/settings-schema.d.ts +69 -21
  30. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  31. package/dist/types/cursor.d.ts +7 -4
  32. package/dist/types/edit/index.d.ts +3 -3
  33. package/dist/types/edit/modes/replace.d.ts +22 -15
  34. package/dist/types/edit/renderer.d.ts +1 -1
  35. package/dist/types/eval/py/kernel.d.ts +3 -1
  36. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  37. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  38. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  39. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  40. package/dist/types/launch/broker.d.ts +1 -1
  41. package/dist/types/launch/client.d.ts +15 -4
  42. package/dist/types/launch/ensure.d.ts +8 -0
  43. package/dist/types/launch/paths.d.ts +1 -1
  44. package/dist/types/launch/protocol.d.ts +19 -2
  45. package/dist/types/lsp/client.d.ts +2 -0
  46. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  47. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  48. package/dist/types/lsp/mux/server.d.ts +18 -0
  49. package/dist/types/lsp/types.d.ts +22 -2
  50. package/dist/types/main.d.ts +2 -0
  51. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  52. package/dist/types/mnemopi/state.d.ts +0 -23
  53. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  54. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  56. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  57. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  58. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  59. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  60. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  61. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  62. package/dist/types/modes/interactive-mode.d.ts +5 -1
  63. package/dist/types/modes/types.d.ts +4 -1
  64. package/dist/types/modes/workflow.d.ts +2 -1
  65. package/dist/types/sdk.d.ts +3 -1
  66. package/dist/types/secrets/index.d.ts +1 -1
  67. package/dist/types/security/contracts/schemas.d.ts +405 -403
  68. package/dist/types/session/agent-session-types.d.ts +8 -1
  69. package/dist/types/session/agent-session.d.ts +26 -2
  70. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  71. package/dist/types/session/launch-completion.d.ts +10 -0
  72. package/dist/types/session/session-advisors.d.ts +1 -1
  73. package/dist/types/session/session-context.d.ts +2 -1
  74. package/dist/types/session/session-entries.d.ts +15 -1
  75. package/dist/types/session/session-maintenance.d.ts +1 -0
  76. package/dist/types/session/session-manager.d.ts +23 -11
  77. package/dist/types/session/session-stats.d.ts +10 -0
  78. package/dist/types/session/session-storage.d.ts +15 -3
  79. package/dist/types/session/yield-queue.d.ts +6 -1
  80. package/dist/types/system-prompt.d.ts +4 -1
  81. package/dist/types/task/index.d.ts +2 -1
  82. package/dist/types/task/spawn-policy.d.ts +5 -0
  83. package/dist/types/thinking.d.ts +1 -7
  84. package/dist/types/tools/ast-grep.d.ts +1 -1
  85. package/dist/types/tools/browser/registry.d.ts +2 -1
  86. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  87. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  88. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  89. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  90. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  91. package/dist/types/tools/browser/render.d.ts +1 -0
  92. package/dist/types/tools/browser.d.ts +3 -0
  93. package/dist/types/tools/computer/exposure.d.ts +9 -7
  94. package/dist/types/tools/computer/protocol.d.ts +70 -17
  95. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  96. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  97. package/dist/types/tools/computer/worker.d.ts +42 -7
  98. package/dist/types/tools/computer-renderer.d.ts +7 -5
  99. package/dist/types/tools/computer.d.ts +25 -44
  100. package/dist/types/tools/context.d.ts +3 -0
  101. package/dist/types/tools/glob.d.ts +1 -1
  102. package/dist/types/tools/grep.d.ts +1 -1
  103. package/dist/types/tools/index.d.ts +9 -0
  104. package/dist/types/tools/render-utils.d.ts +2 -0
  105. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  106. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  107. package/dist/types/tools/xdev.d.ts +8 -0
  108. package/dist/types/utils/clipboard.d.ts +2 -2
  109. package/dist/types/web/kagi.d.ts +1 -0
  110. package/dist/types/web/search/index.d.ts +1 -1
  111. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  112. package/dist/types/web/search/providers/base.d.ts +2 -0
  113. package/dist/types/web/search/providers/brave.d.ts +1 -0
  114. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  115. package/dist/types/web/search/providers/codex.d.ts +1 -0
  116. package/dist/types/web/search/providers/exa.d.ts +1 -0
  117. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  118. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  119. package/dist/types/web/search/providers/jina.d.ts +1 -0
  120. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  121. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  122. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  123. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  124. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  125. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  126. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  127. package/dist/types/web/search/providers/utils.d.ts +6 -9
  128. package/dist/types/web/search/providers/zai.d.ts +1 -0
  129. package/dist/types/web/search/types.d.ts +4 -0
  130. package/package.json +12 -12
  131. package/src/cli/args.ts +7 -87
  132. package/src/cli/browser-relay-cli.ts +119 -0
  133. package/src/cli/command-help.ts +103 -0
  134. package/src/cli/completion-gen.ts +5 -5
  135. package/src/cli/flag-tables.ts +9 -0
  136. package/src/cli/gc-cli.ts +641 -21
  137. package/src/cli/help-extra.ts +89 -0
  138. package/src/cli/setup-cli.ts +13 -21
  139. package/src/cli/thinking-levels.ts +7 -0
  140. package/src/cli/update-cli.ts +26 -7
  141. package/src/cli-commands.ts +170 -33
  142. package/src/cli.ts +17 -5
  143. package/src/commands/acp.ts +3 -1
  144. package/src/commands/agents.ts +3 -2
  145. package/src/commands/auth-broker.ts +3 -2
  146. package/src/commands/auth-gateway.ts +3 -2
  147. package/src/commands/bench.ts +2 -3
  148. package/src/commands/browser-relay.ts +53 -0
  149. package/src/commands/cleanse.ts +2 -2
  150. package/src/commands/commit.ts +3 -2
  151. package/src/commands/complete.ts +2 -1
  152. package/src/commands/completions.ts +3 -2
  153. package/src/commands/config.ts +3 -2
  154. package/src/commands/dry-balance.ts +2 -2
  155. package/src/commands/gallery.ts +3 -2
  156. package/src/commands/gc.ts +3 -2
  157. package/src/commands/grep.ts +3 -2
  158. package/src/commands/grievances.ts +3 -2
  159. package/src/commands/install.ts +2 -2
  160. package/src/commands/join.ts +3 -2
  161. package/src/commands/launch-help.ts +116 -0
  162. package/src/commands/launch.ts +7 -191
  163. package/src/commands/models.ts +3 -2
  164. package/src/commands/plugin.ts +3 -2
  165. package/src/commands/read.ts +3 -2
  166. package/src/commands/say.ts +3 -2
  167. package/src/commands/setup.ts +3 -2
  168. package/src/commands/shell.ts +3 -2
  169. package/src/commands/ssh.ts +3 -2
  170. package/src/commands/stats.ts +3 -2
  171. package/src/commands/tiny-models.ts +2 -2
  172. package/src/commands/token.ts +2 -2
  173. package/src/commands/ttsr.ts +2 -2
  174. package/src/commands/update.ts +3 -2
  175. package/src/commands/usage.ts +3 -2
  176. package/src/commands/web-search.ts +3 -2
  177. package/src/commands/worktree.ts +3 -2
  178. package/src/config/keybindings.ts +5 -0
  179. package/src/config/model-discovery.ts +44 -27
  180. package/src/config/model-registry.ts +19 -13
  181. package/src/config/models-config-schema-bundle.ts +9 -0
  182. package/src/config/resolve-config-value.ts +2 -1
  183. package/src/config/service-tier.ts +5 -0
  184. package/src/config/settings-schema.ts +72 -20
  185. package/src/config/settings.ts +1 -1
  186. package/src/cursor-bridge-tools.ts +1 -1
  187. package/src/cursor.ts +10 -14
  188. package/src/edit/diff.ts +17 -8
  189. package/src/edit/index.ts +20 -7
  190. package/src/edit/modes/replace.ts +93 -40
  191. package/src/edit/renderer.ts +1 -1
  192. package/src/edit/streaming.ts +14 -17
  193. package/src/eval/py/kernel.ts +2 -1
  194. package/src/eval/py/spawn-options.ts +24 -29
  195. package/src/export/html/index.ts +10 -3
  196. package/src/export/html/template.js +8 -26
  197. package/src/export/share.ts +4 -0
  198. package/src/extensibility/extensions/wrapper.ts +1 -0
  199. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  200. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  201. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  202. package/src/extensibility/skills.ts +11 -0
  203. package/src/extensibility/utils.ts +10 -2
  204. package/src/launch/broker.ts +223 -7
  205. package/src/launch/client.ts +192 -21
  206. package/src/launch/ensure.ts +73 -0
  207. package/src/launch/paths.ts +1 -1
  208. package/src/launch/protocol.ts +54 -2
  209. package/src/lsp/client.ts +23 -2
  210. package/src/lsp/index.ts +8 -0
  211. package/src/lsp/mux/daemon.ts +348 -0
  212. package/src/lsp/mux/protocol.ts +96 -0
  213. package/src/lsp/mux/server.ts +797 -0
  214. package/src/lsp/types.ts +28 -2
  215. package/src/main.ts +20 -8
  216. package/src/mcp/config-writer.ts +1 -1
  217. package/src/mcp/oauth-flow.ts +11 -1
  218. package/src/mnemopi/embed-client.ts +49 -2
  219. package/src/mnemopi/state.ts +57 -23
  220. package/src/modes/components/assistant-message.ts +11 -0
  221. package/src/modes/components/btw-panel.ts +41 -4
  222. package/src/modes/components/chat-transcript-builder.ts +4 -0
  223. package/src/modes/components/custom-editor.ts +9 -0
  224. package/src/modes/components/read-tool-group.ts +10 -0
  225. package/src/modes/components/status-line/component.ts +1 -1
  226. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  227. package/src/modes/components/tool-execution.ts +8 -0
  228. package/src/modes/components/tree-selector.ts +46 -48
  229. package/src/modes/controllers/btw-controller.ts +55 -7
  230. package/src/modes/controllers/command-controller.ts +31 -0
  231. package/src/modes/controllers/event-controller.ts +17 -0
  232. package/src/modes/controllers/input-controller.ts +63 -8
  233. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  234. package/src/modes/controllers/selector-controller.ts +20 -0
  235. package/src/modes/interactive-mode.ts +18 -2
  236. package/src/modes/types.ts +9 -1
  237. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  238. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  239. package/src/modes/utils/ui-helpers.ts +6 -10
  240. package/src/modes/workflow.ts +8 -2
  241. package/src/prompts/agents/init.md +1 -1
  242. package/src/prompts/session/launch-completion.md +1 -0
  243. package/src/prompts/system/plan-mode-active.md +2 -2
  244. package/src/prompts/system/system-prompt.md +26 -52
  245. package/src/prompts/system/workflow-notice.md +3 -3
  246. package/src/prompts/tools/ast-grep.md +1 -1
  247. package/src/prompts/tools/bash.md +1 -1
  248. package/src/prompts/tools/browser.md +1 -0
  249. package/src/prompts/tools/computer.md +21 -21
  250. package/src/prompts/tools/glob.md +1 -1
  251. package/src/prompts/tools/grep.md +1 -1
  252. package/src/prompts/tools/replace.md +5 -4
  253. package/src/prompts/tools/task.md +4 -4
  254. package/src/sdk.ts +49 -6
  255. package/src/secrets/index.ts +1 -1
  256. package/src/security/contracts/schemas.ts +205 -183
  257. package/src/security/contracts/validation.ts +5 -1
  258. package/src/security/store.ts +2 -2
  259. package/src/session/agent-session-types.ts +9 -1
  260. package/src/session/agent-session.ts +204 -15
  261. package/src/session/indexed-session-storage.ts +9 -0
  262. package/src/session/launch-completion.ts +37 -0
  263. package/src/session/prewalk.ts +24 -2
  264. package/src/session/session-advisors.ts +2 -2
  265. package/src/session/session-context.ts +42 -12
  266. package/src/session/session-dump-format.ts +1 -1
  267. package/src/session/session-entries.ts +17 -1
  268. package/src/session/session-maintenance.ts +38 -4
  269. package/src/session/session-manager.ts +123 -67
  270. package/src/session/session-paths.ts +118 -55
  271. package/src/session/session-stats.ts +48 -3
  272. package/src/session/session-storage.ts +30 -22
  273. package/src/session/stream-guards.ts +1 -1
  274. package/src/session/yield-queue.ts +121 -16
  275. package/src/slash-commands/builtin-registry.ts +37 -24
  276. package/src/system-prompt.ts +8 -3
  277. package/src/task/index.ts +23 -8
  278. package/src/task/spawn-policy.ts +14 -0
  279. package/src/thinking.ts +2 -8
  280. package/src/tools/ast-grep.ts +10 -4
  281. package/src/tools/browser/cmux/cmux-tab.ts +97 -14
  282. package/src/tools/browser/cmux/rpc.ts +2 -8
  283. package/src/tools/browser/registry.ts +53 -2
  284. package/src/tools/browser/relay/bridge.ts +945 -0
  285. package/src/tools/browser/relay/daemon.ts +117 -0
  286. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  287. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  288. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  289. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  290. package/src/tools/browser/relay/kind.ts +41 -0
  291. package/src/tools/browser/relay/protocol.ts +54 -0
  292. package/src/tools/browser/relay/server.ts +141 -0
  293. package/src/tools/browser/render.ts +4 -1
  294. package/src/tools/browser/shared-daemon.ts +7 -58
  295. package/src/tools/browser/tab-supervisor.ts +5 -4
  296. package/src/tools/browser/tab-worker.ts +33 -12
  297. package/src/tools/browser.ts +26 -1
  298. package/src/tools/computer/exposure.ts +11 -35
  299. package/src/tools/computer/protocol.ts +51 -10
  300. package/src/tools/computer/supervisor.ts +227 -124
  301. package/src/tools/computer/worker-entry.ts +11 -6
  302. package/src/tools/computer/worker.ts +695 -81
  303. package/src/tools/computer-renderer.ts +104 -65
  304. package/src/tools/computer.ts +134 -428
  305. package/src/tools/context.ts +3 -0
  306. package/src/tools/glob.ts +9 -2
  307. package/src/tools/grep.ts +12 -6
  308. package/src/tools/hub/launch.ts +122 -6
  309. package/src/tools/index.ts +9 -0
  310. package/src/tools/inspect-image.ts +32 -6
  311. package/src/tools/render-utils.ts +2 -0
  312. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  313. package/src/tools/tool-timeouts.ts +1 -0
  314. package/src/tools/xdev.ts +29 -2
  315. package/src/utils/clipboard.ts +40 -14
  316. package/src/utils/command-args.ts +26 -28
  317. package/src/utils/tool-choice.ts +0 -13
  318. package/src/web/kagi.ts +2 -1
  319. package/src/web/search/index.ts +18 -2
  320. package/src/web/search/providers/anthropic.ts +4 -1
  321. package/src/web/search/providers/base.ts +2 -0
  322. package/src/web/search/providers/brave.ts +3 -1
  323. package/src/web/search/providers/browser-page.ts +6 -4
  324. package/src/web/search/providers/codex.ts +4 -1
  325. package/src/web/search/providers/duckduckgo.ts +2 -1
  326. package/src/web/search/providers/ecosia.ts +2 -1
  327. package/src/web/search/providers/exa.ts +4 -2
  328. package/src/web/search/providers/firecrawl.ts +3 -1
  329. package/src/web/search/providers/gemini.ts +8 -2
  330. package/src/web/search/providers/google.ts +2 -1
  331. package/src/web/search/providers/jina.ts +12 -2
  332. package/src/web/search/providers/kagi.ts +3 -0
  333. package/src/web/search/providers/kimi.ts +5 -1
  334. package/src/web/search/providers/mojeek.ts +2 -1
  335. package/src/web/search/providers/parallel.ts +5 -1
  336. package/src/web/search/providers/perplexity.ts +6 -3
  337. package/src/web/search/providers/public.ts +1 -1
  338. package/src/web/search/providers/searxng.ts +11 -5
  339. package/src/web/search/providers/startpage.ts +10 -4
  340. package/src/web/search/providers/synthetic.ts +11 -5
  341. package/src/web/search/providers/tavily.ts +3 -1
  342. package/src/web/search/providers/tinyfish.ts +3 -1
  343. package/src/web/search/providers/utils.ts +12 -10
  344. package/src/web/search/providers/xai.ts +2 -2
  345. package/src/web/search/providers/zai.ts +18 -3
  346. package/src/web/search/types.ts +6 -0
  347. package/dist/types/config/file-lock.d.ts +0 -29
  348. 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
+ }
@@ -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 project broker. */
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");
@@ -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 project directory. */
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: Bun.FileSink,
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
- const proc = ptree.spawn([command, ...args], {
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
+ }