oh-my-opencode 5.0.0-beta.30 → 5.0.0-beta.32

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 (278) hide show
  1. package/dist/cli/index.js +178 -37
  2. package/dist/cli-node/index.js +178 -37
  3. package/dist/config/schema/background-task.d.ts +1 -0
  4. package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
  5. package/dist/features/background-agent/constants.d.ts +0 -7
  6. package/dist/features/background-agent/manager.d.ts +2 -1
  7. package/dist/features/background-agent/parent-wake-dispatched-tracker.d.ts +0 -1
  8. package/dist/features/background-agent/parent-wake-notifier-types.d.ts +1 -1
  9. package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -1
  10. package/dist/features/background-agent/parent-wake-session-inspector.d.ts +1 -1
  11. package/dist/features/background-agent/subagent-spawn-limits.d.ts +8 -0
  12. package/dist/features/monitor/manager-internals.d.ts +1 -1
  13. package/dist/features/monitor/output-injector-types.d.ts +1 -1
  14. package/dist/features/task-toast-manager/manager.d.ts +0 -24
  15. package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +1 -0
  16. package/dist/features/team-mode/tools/messaging-runtime.d.ts +1 -0
  17. package/dist/hooks/anthropic-context-window-limit-recovery/message-storage-directory.d.ts +0 -3
  18. package/dist/hooks/anthropic-context-window-limit-recovery/state.d.ts +1 -0
  19. package/dist/hooks/anthropic-context-window-limit-recovery/storage/empty-messages.d.ts +0 -1
  20. package/dist/hooks/anthropic-context-window-limit-recovery/tool-result-storage.d.ts +11 -0
  21. package/dist/hooks/claude-code-hooks/session-hook-state.d.ts +1 -0
  22. package/dist/hooks/claude-code-hooks/stop.d.ts +2 -0
  23. package/dist/hooks/claude-code-hooks/todo.d.ts +0 -1
  24. package/dist/hooks/claude-code-hooks/transcript.d.ts +2 -0
  25. package/dist/hooks/fsync-skip-warning/index.d.ts +4 -0
  26. package/dist/hooks/hashline-edit-diff-enhancer/hook.d.ts +1 -0
  27. package/dist/hooks/hashline-edit-diff-enhancer/pending-captures.d.ts +13 -0
  28. package/dist/hooks/keyword-detector/hook.d.ts +7 -0
  29. package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +0 -1
  30. package/dist/hooks/model-fallback/hook.d.ts +1 -1
  31. package/dist/index.js +681 -273
  32. package/dist/oh-my-opencode.schema.json +5 -0
  33. package/dist/plugin-dispose.d.ts +3 -0
  34. package/dist/shared/agent-tool-restrictions.d.ts +0 -1
  35. package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  36. package/dist/tools/delegate-task/sync-session-cleanup.d.ts +3 -0
  37. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -0
  38. package/dist/tools/delegate-task/types.d.ts +1 -0
  39. package/dist/tools/session-manager/directory-filter.d.ts +2 -0
  40. package/dist/tui.js +148 -9
  41. package/package.json +16 -15
  42. package/packages/git-bash-mcp/dist/cli.js +1 -0
  43. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +47 -19
  44. package/packages/lsp-core/src/lsp/client-diagnostics-pull-timeout.test.ts +64 -0
  45. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +97 -0
  46. package/packages/lsp-core/src/lsp/client-wrapper.ts +21 -6
  47. package/packages/lsp-core/src/lsp/client.ts +6 -3
  48. package/packages/lsp-core/src/lsp/constants.ts +5 -0
  49. package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
  50. package/packages/lsp-core/src/lsp/errors.ts +15 -0
  51. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
  52. package/packages/lsp-core/src/lsp/manager-lifecycle.test.ts +262 -0
  53. package/packages/lsp-core/src/lsp/manager.ts +106 -13
  54. package/packages/lsp-core/src/lsp/timer-provider.ts +13 -0
  55. package/packages/lsp-core/src/lsp/transport-request-timeout.test.ts +73 -0
  56. package/packages/lsp-core/src/lsp/transport.ts +7 -2
  57. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +84 -1
  58. package/packages/lsp-core/src/lsp/workspace-document-state.ts +30 -11
  59. package/packages/lsp-core/src/lsp/workspace-markers.ts +18 -1
  60. package/packages/lsp-core/src/mcp.ts +3 -0
  61. package/packages/lsp-core/src/tools/parameters.test.ts +51 -0
  62. package/packages/lsp-daemon/dist/cli.js +155 -34
  63. package/packages/lsp-daemon/dist/client.js +155 -34
  64. package/packages/lsp-daemon/dist/index.js +155 -34
  65. package/packages/lsp-daemon/dist/proxy.js +3 -0
  66. package/packages/lsp-tools-mcp/dist/cli.js +155 -34
  67. package/packages/lsp-tools-mcp/dist/lsp/manager.js +134 -28
  68. package/packages/lsp-tools-mcp/dist/mcp.js +155 -34
  69. package/packages/lsp-tools-mcp/dist/tools.js +154 -34
  70. package/packages/lsp-tools-mcp/package.json +1 -1
  71. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  72. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  73. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  74. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +17 -2
  75. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +17 -2
  76. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +12 -0
  78. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +47 -0
  79. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  80. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  81. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  82. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  83. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  84. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  85. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  86. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +152 -34
  87. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  88. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  89. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  90. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  91. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  92. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  93. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  94. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  95. package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -4
  96. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +489 -4
  97. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  98. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -2
  99. package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +30 -14
  100. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +4 -0
  101. package/packages/omo-codex/plugin/components/ultrawork/src/directive.ts +5 -2
  102. package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +14 -8
  103. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +12 -1
  104. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  105. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  106. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +5 -2
  107. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -0
  108. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +9 -0
  109. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +14 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +10 -1
  111. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +8 -0
  112. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +113 -0
  113. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +9 -5
  114. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
  115. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  116. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
  118. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +856 -347
  119. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.d.ts +2 -0
  120. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +27 -6
  121. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +13 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +14 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +17 -9
  124. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +17 -2
  125. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +13 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +19 -0
  127. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +2 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +53 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.d.ts +9 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +85 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
  132. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +4 -2
  133. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +41 -102
  134. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +4 -2
  135. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +185 -14
  136. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +6 -0
  137. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +33 -0
  138. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +18 -0
  139. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +65 -0
  140. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  141. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  142. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +17 -0
  143. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +7 -0
  144. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +25 -1
  145. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +127 -0
  146. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +10 -12
  147. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +2 -1
  148. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -0
  149. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
  150. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +34 -5
  151. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +23 -0
  152. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +19 -9
  153. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +18 -3
  154. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +27 -0
  155. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +57 -0
  156. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +103 -0
  157. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +1 -1
  158. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +54 -123
  159. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +4 -2
  160. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +187 -12
  161. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +36 -0
  162. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +95 -0
  163. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +107 -1
  164. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +92 -0
  165. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +183 -0
  166. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +35 -0
  167. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +7 -3
  168. package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +105 -0
  169. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +101 -0
  170. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +46 -2
  171. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-senpi-surface.test.ts +131 -0
  172. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +5 -0
  173. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +318 -1
  174. package/packages/omo-codex/plugin/components/ulw-loop/test/surface.test.ts +74 -0
  175. package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +9 -3
  176. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  179. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  180. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  181. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  182. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  183. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  184. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  185. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  186. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  193. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  194. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  195. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  196. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  197. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  198. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  199. package/packages/omo-codex/plugin/package-lock.json +13 -13
  200. package/packages/omo-codex/plugin/package.json +1 -1
  201. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +31 -2
  202. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  203. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +17 -0
  204. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +11 -1
  205. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +13 -0
  206. package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +5 -2
  207. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  208. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  209. package/dist/features/builtin-skills/index.d.ts +0 -1
  210. package/dist/features/builtin-skills/skill-file-loader.d.ts +0 -1
  211. package/dist/features/builtin-skills/skills/agent-browser-skill.d.ts +0 -1
  212. package/dist/features/builtin-skills/skills/agent-browser-template.d.ts +0 -1
  213. package/dist/features/builtin-skills/skills/debugging.d.ts +0 -1
  214. package/dist/features/builtin-skills/skills/dev-browser.d.ts +0 -1
  215. package/dist/features/builtin-skills/skills/frontend.d.ts +0 -1
  216. package/dist/features/builtin-skills/skills/git-master-sections/commit-atomic-planning.d.ts +0 -1
  217. package/dist/features/builtin-skills/skills/git-master-sections/commit-context-analysis.d.ts +0 -1
  218. package/dist/features/builtin-skills/skills/git-master-sections/commit-execution-verification.d.ts +0 -1
  219. package/dist/features/builtin-skills/skills/git-master-sections/commit-workflow.d.ts +0 -1
  220. package/dist/features/builtin-skills/skills/git-master-sections/history-search-workflow.d.ts +0 -1
  221. package/dist/features/builtin-skills/skills/git-master-sections/overview.d.ts +0 -1
  222. package/dist/features/builtin-skills/skills/git-master-sections/quick-reference.d.ts +0 -1
  223. package/dist/features/builtin-skills/skills/git-master-sections/rebase-workflow.d.ts +0 -1
  224. package/dist/features/builtin-skills/skills/git-master-skill-metadata.d.ts +0 -1
  225. package/dist/features/builtin-skills/skills/git-master.d.ts +0 -1
  226. package/dist/features/builtin-skills/skills/index.d.ts +0 -1
  227. package/dist/features/builtin-skills/skills/init-deep.d.ts +0 -1
  228. package/dist/features/builtin-skills/skills/playwright-cli.d.ts +0 -1
  229. package/dist/features/builtin-skills/skills/playwright-mcp-skill.d.ts +0 -1
  230. package/dist/features/builtin-skills/skills/playwright.d.ts +0 -1
  231. package/dist/features/builtin-skills/skills/remove-ai-slops.d.ts +0 -1
  232. package/dist/features/builtin-skills/skills/review-work.d.ts +0 -1
  233. package/dist/features/builtin-skills/skills/security-research.d.ts +0 -1
  234. package/dist/features/builtin-skills/skills/security-review.d.ts +0 -1
  235. package/dist/features/builtin-skills/skills/team-mode.d.ts +0 -1
  236. package/dist/features/builtin-skills/skills/visual-qa.d.ts +0 -1
  237. package/dist/features/builtin-skills/skills.d.ts +0 -1
  238. package/dist/features/builtin-skills/types.d.ts +0 -1
  239. package/dist/features/claude-code-plugin-loader/agent-loader.d.ts +0 -1
  240. package/dist/features/claude-code-plugin-loader/command-loader.d.ts +0 -1
  241. package/dist/features/claude-code-plugin-loader/discovery-core.d.ts +0 -1
  242. package/dist/features/claude-code-plugin-loader/discovery-paths.d.ts +0 -1
  243. package/dist/features/claude-code-plugin-loader/discovery.d.ts +0 -1
  244. package/dist/features/claude-code-plugin-loader/hook-loader.d.ts +0 -1
  245. package/dist/features/claude-code-plugin-loader/index.d.ts +0 -1
  246. package/dist/features/claude-code-plugin-loader/install-path-resolver.d.ts +0 -1
  247. package/dist/features/claude-code-plugin-loader/installed-plugin-database.d.ts +0 -1
  248. package/dist/features/claude-code-plugin-loader/loaded-plugin.d.ts +0 -1
  249. package/dist/features/claude-code-plugin-loader/loader.d.ts +0 -1
  250. package/dist/features/claude-code-plugin-loader/mcp-server-loader.d.ts +0 -1
  251. package/dist/features/claude-code-plugin-loader/plugin-key.d.ts +0 -1
  252. package/dist/features/claude-code-plugin-loader/plugin-manifest.d.ts +0 -1
  253. package/dist/features/claude-code-plugin-loader/plugin-path-resolver.d.ts +0 -1
  254. package/dist/features/claude-code-plugin-loader/plugin-settings.d.ts +0 -1
  255. package/dist/features/claude-code-plugin-loader/scope-filter.d.ts +0 -1
  256. package/dist/features/claude-code-plugin-loader/skill-loader.d.ts +0 -1
  257. package/dist/features/claude-code-plugin-loader/types.d.ts +0 -1
  258. package/dist/hooks/ralph-loop/completion-promise-detector-test-input.d.ts +0 -11
  259. package/dist/hooks/ralph-loop/loop-session-recovery.d.ts +0 -7
  260. package/dist/shared/prompt-async-gate/message-inspection-error.d.ts +0 -1
  261. package/dist/shared/prompt-async-gate/pending-tool-turn.d.ts +0 -1
  262. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +0 -1
  263. package/dist/shared/prompt-async-gate/queue.d.ts +0 -1
  264. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +0 -1
  265. package/dist/shared/prompt-async-gate/reservations.d.ts +0 -1
  266. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +0 -1
  267. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +0 -1
  268. package/dist/shared/prompt-async-gate/timing.d.ts +0 -1
  269. package/dist/shared/prompt-async-gate/types.d.ts +0 -1
  270. package/dist/tools/call-omo-agent/message-storage-directory.d.ts +0 -1
  271. package/dist/tools/delegate-task/model-string-parser.d.ts +0 -1
  272. package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
  273. package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
  274. package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
  275. package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +0 -1
  276. package/packages/lsp-tools-mcp/dist/missing-dependency-result.d.ts +0 -1
  277. package/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
  278. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { afterEach, describe, expect, it } from "bun:test";
5
5
 
6
6
  import { createStandaloneMcpRequestContext, runWithRequestContext } from "../request-context.js";
7
+ import type { LspClient } from "./client.js";
7
8
  import { findWorkspaceRoot, resolveReadablePathInsideContext, withLspClient } from "./client-wrapper.js";
8
9
  import { LspInvalidPathError } from "./errors.js";
9
10
  import { LspManager } from "./manager.js";
@@ -22,6 +23,102 @@ function tempRoot(prefix: string): string {
22
23
  return root;
23
24
  }
24
25
 
26
+ describe("LSP workspace root collapse in monorepos", () => {
27
+ it("#given a git repository containing four package.json packages #when resolving workspace roots for files in each package #then all collapse to the repository root", () => {
28
+ const root = tempRoot("lsp-client-wrapper-monorepo-");
29
+ mkdirSync(join(root, ".git"), { recursive: true });
30
+ const packageDirs = ["alpha", "beta", "gamma", "delta"].map((name) => join(root, "packages", name));
31
+ for (const packageDir of packageDirs) {
32
+ mkdirSync(join(packageDir, "src"), { recursive: true });
33
+ writeFileSync(join(packageDir, "package.json"), "{}\n");
34
+ writeFileSync(join(packageDir, "src", "file.ts"), "export const value = 1;\n");
35
+ }
36
+ const context = createStandaloneMcpRequestContext({ cwd: root });
37
+
38
+ const roots = packageDirs.map((packageDir) =>
39
+ runWithRequestContext(context, () => findWorkspaceRoot(join(packageDir, "src", "file.ts"))),
40
+ );
41
+
42
+ expect(new Set(roots).size).toBe(1);
43
+ expect(roots[0]).toBe(realpathSync(root));
44
+ });
45
+
46
+ it("#given nested package.json markers without any .git #when resolving workspace #then keeps the nearest package root", () => {
47
+ const root = tempRoot("lsp-client-wrapper-nested-");
48
+ const nested = join(root, "parent", "nested");
49
+ mkdirSync(join(nested, "src"), { recursive: true });
50
+ writeFileSync(join(root, "parent", "package.json"), "{}\n");
51
+ writeFileSync(join(nested, "package.json"), "{}\n");
52
+ writeFileSync(join(nested, "src", "file.ts"), "export const value = 1;\n");
53
+
54
+ const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
55
+ findWorkspaceRoot(join(nested, "src", "file.ts")),
56
+ );
57
+
58
+ expect(workspace).toBe(realpathSync(nested));
59
+ });
60
+
61
+ it("#given four monorepo package files under one .git root #when acquiring clients #then one client serves the whole repository", async () => {
62
+ const root = tempRoot("lsp-client-wrapper-oneclient-");
63
+ mkdirSync(join(root, ".git"), { recursive: true });
64
+ const userConfig = join(root, "user-lsp.json");
65
+ writeFileSync(
66
+ userConfig,
67
+ JSON.stringify({ lsp: { monorepoProbe: { command: [process.execPath], extensions: [".monoroot"] } } }),
68
+ );
69
+ const filePaths = ["alpha", "beta", "gamma", "delta"].map((name) => {
70
+ const packageDir = join(root, "packages", name);
71
+ mkdirSync(join(packageDir, "src"), { recursive: true });
72
+ writeFileSync(join(packageDir, "package.json"), "{}\n");
73
+ const filePath = join(packageDir, "src", "probe.monoroot");
74
+ writeFileSync(filePath, "const value = 1;\n");
75
+ return filePath;
76
+ });
77
+ const factoryRoots: string[] = [];
78
+ const manager = new LspManager({
79
+ clientFactory: (workspaceRoot) => {
80
+ factoryRoots.push(workspaceRoot);
81
+ return new StubLspClient() as unknown as LspClient;
82
+ },
83
+ });
84
+
85
+ try {
86
+ const context = createStandaloneMcpRequestContext({
87
+ cwd: root,
88
+ env: { LSP_TOOLS_MCP_USER_CONFIG: userConfig },
89
+ });
90
+ await Promise.all(
91
+ filePaths.map((filePath) =>
92
+ runWithRequestContext(context, () =>
93
+ withLspClient(filePath, async () => undefined, "diagnostics", { manager }),
94
+ ),
95
+ ),
96
+ );
97
+
98
+ expect(factoryRoots).toEqual([realpathSync(root)]);
99
+ expect(manager.clientCount()).toBe(1);
100
+ } finally {
101
+ await manager.stopAll();
102
+ }
103
+ });
104
+ });
105
+
106
+ class StubLspClient {
107
+ async start(): Promise<void> {}
108
+
109
+ async initialize(): Promise<void> {}
110
+
111
+ isAlive(): boolean {
112
+ return true;
113
+ }
114
+
115
+ command(): string[] {
116
+ return [process.execPath];
117
+ }
118
+
119
+ async stop(): Promise<void> {}
120
+ }
121
+
25
122
  describe("LSP client path confinement", () => {
26
123
  it("#given a relative file inside context cwd #when resolving workspace #then marker search stays inside cwd", () => {
27
124
  const root = tempRoot("lsp-client-wrapper-root-");
@@ -21,7 +21,7 @@ import { LSP_LOCAL_INSTALL_HINTS } from "./server-definitions.js";
21
21
  import { loadInstallDecision } from "./server-install-state.js";
22
22
  import { findServerForExtension } from "./server-resolution.js";
23
23
  import type { ServerLookupResult } from "./types.js";
24
- import { WORKSPACE_MARKERS } from "./workspace-markers.js";
24
+ import { GIT_WORKSPACE_MARKER, PROJECT_WORKSPACE_MARKERS } from "./workspace-markers.js";
25
25
 
26
26
  export function isDirectoryPath(filePath: string): boolean {
27
27
  try {
@@ -31,6 +31,15 @@ export function isDirectoryPath(filePath: string): boolean {
31
31
  }
32
32
  }
33
33
 
34
+ /**
35
+ * Resolves the workspace root for a file inside the request cwd.
36
+ *
37
+ * A `.git`-containing ancestor always wins over nearer package markers, so all packages of a
38
+ * monorepo share one workspace root (one resident client per repository instead of one per
39
+ * package, which multiplies language-server processes by the package count). The nearest
40
+ * package-marked directory is remembered while walking and used only when no `.git` ancestor
41
+ * exists within the request cwd.
42
+ */
34
43
  export function findWorkspaceRoot(filePath: string): string {
35
44
  const cwd = contextCwd();
36
45
  const abs = resolveReadablePathInsideContext(filePath);
@@ -43,20 +52,26 @@ export function findWorkspaceRoot(filePath: string): string {
43
52
  if (!isPathInside(cwd, abs)) return findWorkspaceRootOutsideContext(dir);
44
53
 
45
54
  const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
55
+ let nearestPackageRoot: string | undefined;
46
56
  while (isPathInside(cwd, dir)) {
47
57
  const canonicalDir = existingDirectoryInsideContext(dir, cwd);
48
58
  if (canonicalDir !== undefined) {
49
- for (const marker of WORKSPACE_MARKERS) {
50
- if (existsSync(join(dir, marker))) {
51
- return canonicalDir;
52
- }
59
+ if (existsSync(join(dir, GIT_WORKSPACE_MARKER))) {
60
+ return canonicalDir;
61
+ }
62
+ if (nearestPackageRoot === undefined && hasProjectWorkspaceMarker(dir)) {
63
+ nearestPackageRoot = canonicalDir;
53
64
  }
54
65
  }
55
66
  if (dir === cwd) break;
56
67
  dir = dirname(dir);
57
68
  }
58
69
 
59
- return fallbackRoot;
70
+ return nearestPackageRoot ?? fallbackRoot;
71
+ }
72
+
73
+ function hasProjectWorkspaceMarker(directory: string): boolean {
74
+ return PROJECT_WORKSPACE_MARKERS.some((marker) => existsSync(join(directory, marker)));
60
75
  }
61
76
 
62
77
  export function resolveReadablePathInsideContext(filePath: string): string {
@@ -19,6 +19,7 @@ import type {
19
19
  } from "./types.js";
20
20
  import { LspRequestTimeoutError } from "./errors.js";
21
21
  import { WorkspaceDocumentState } from "./workspace-document-state.js";
22
+ import type { TimerProvider } from "./timer-provider.js";
22
23
  import type { LspRenameResult, WorkspaceEditCommitIo } from "./workspace-edit-types.js";
23
24
  import { WorkspaceMutationController } from "./workspace-mutation-controller.js";
24
25
 
@@ -36,6 +37,7 @@ export interface LspDiagnosticsResult {
36
37
  export interface LspClientOptions extends LspClientTimeoutOptions {
37
38
  readonly diagnosticsFreshnessTimeoutMs?: number;
38
39
  readonly versionlessPublishQuiescenceMs?: number;
40
+ readonly timerProvider?: TimerProvider;
39
41
  }
40
42
 
41
43
  export class LspClient extends LspClientConnection {
@@ -52,6 +54,7 @@ export class LspClient extends LspClientConnection {
52
54
  (method, params) => this.sendNotification(method, params),
53
55
  (uri) => this.diagnosticsStore.delete(uri),
54
56
  {
57
+ timerProvider: this.timerProvider,
55
58
  versionlessPublishQuiescenceMs:
56
59
  options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS,
57
60
  },
@@ -207,7 +210,7 @@ export class LspClient extends LspClientConnection {
207
210
  const absPath = this.resolveWorkspacePath(filePath);
208
211
  const uri = pathToFileURL(absPath).href;
209
212
  await this.openFile(absPath);
210
- const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
213
+ const deadlineAt = this.timerProvider.now() + this.diagnosticsFreshnessTimeoutMs;
211
214
 
212
215
  for (;;) {
213
216
  signal?.throwIfAborted();
@@ -220,7 +223,7 @@ export class LspClient extends LspClientConnection {
220
223
  if (!pushFallbackOnly) {
221
224
  const cached = this.documents.getPullCache(snapshot);
222
225
  try {
223
- const remainingMs = deadlineAt - Date.now();
226
+ const remainingMs = deadlineAt - this.timerProvider.now();
224
227
  if (remainingMs <= 0) return this.freshnessTimeout(absPath);
225
228
  const result = await this.sendRequest<{ items?: Diagnostic[]; kind?: string; resultId?: string }>(
226
229
  "textDocument/diagnostic",
@@ -262,7 +265,7 @@ export class LspClient extends LspClientConnection {
262
265
 
263
266
  if (!pushFallbackOnly) continue;
264
267
 
265
- const remainingMs = deadlineAt - Date.now();
268
+ const remainingMs = deadlineAt - this.timerProvider.now();
266
269
  if (remainingMs <= 0) {
267
270
  // A server that never advertised pull diagnostics (or rejected the pull
268
271
  // method) and has never published for this document stays silent for a
@@ -10,3 +10,8 @@ export const MAX_RESIDENT_CLIENTS = 6;
10
10
  export const REAPER_INTERVAL_MS = 60_000;
11
11
  export const STOP_HARD_KILL_TIMEOUT_MS = 5_000;
12
12
  export const STOP_SIGKILL_GRACE_MS = 1_000;
13
+
14
+ /** Max consecutive dead client generations respawned for one key before admission is blocked. */
15
+ export const CLIENT_RESPAWN_RETRY_LIMIT = 2;
16
+ /** How long a spent respawn budget blocks admission before a fresh attempt window opens. */
17
+ export const CLIENT_RESPAWN_COOLDOWN_MS = 60_000;
@@ -0,0 +1,61 @@
1
+ import type { TimerProvider } from "./timer-provider.js";
2
+
3
+ interface ControlledTimer {
4
+ readonly at: number;
5
+ readonly callback: () => void;
6
+ cancelled: boolean;
7
+ }
8
+
9
+ interface TimerWaiter {
10
+ readonly delayMs: number | undefined;
11
+ readonly resolve: () => void;
12
+ }
13
+
14
+ /**
15
+ * Deterministic TimerProvider for tests: timers fire only through advanceBy(),
16
+ * and waitForTimer(delayMs) resolves once a timer with exactly that delay is
17
+ * scheduled, so tests order themselves behind the timer they intend to fire.
18
+ */
19
+ export class ControlledClock implements TimerProvider {
20
+ private readonly timers: ControlledTimer[] = [];
21
+ private currentTime = 0;
22
+ private readonly timerWaiters = new Set<TimerWaiter>();
23
+ readonly scheduledDelays: number[] = [];
24
+
25
+ readonly now = (): number => this.currentTime;
26
+ readonly setTimeout = (callback: () => void, delayMs: number): ReturnType<typeof setTimeout> => {
27
+ const timer: ControlledTimer = { at: this.currentTime + delayMs, callback, cancelled: false };
28
+ this.scheduledDelays.push(delayMs);
29
+ this.timers.push(timer);
30
+ for (const waiter of this.timerWaiters) {
31
+ if (waiter.delayMs === undefined || waiter.delayMs === delayMs) {
32
+ this.timerWaiters.delete(waiter);
33
+ waiter.resolve();
34
+ }
35
+ }
36
+ return timer as unknown as ReturnType<typeof setTimeout>;
37
+ };
38
+ readonly clearTimeout = (handle: ReturnType<typeof setTimeout>): void => {
39
+ (handle as unknown as { cancelled: boolean }).cancelled = true;
40
+ };
41
+
42
+ waitForTimer(delayMs?: number): Promise<void> {
43
+ const hasTimer = this.timers.some(
44
+ (timer) => !timer.cancelled && (delayMs === undefined || timer.at === this.currentTime + delayMs),
45
+ );
46
+ if (hasTimer) return Promise.resolve();
47
+ return new Promise((resolve) => {
48
+ this.timerWaiters.add({ delayMs, resolve });
49
+ });
50
+ }
51
+
52
+ advanceBy(delayMs: number): void {
53
+ this.currentTime += delayMs;
54
+ for (;;) {
55
+ const index = this.timers.findIndex((timer) => timer.at <= this.currentTime);
56
+ if (index < 0) return;
57
+ const timer = this.timers.splice(index, 1)[0];
58
+ if (timer && !timer.cancelled) timer.callback();
59
+ }
60
+ }
61
+ }
@@ -69,6 +69,21 @@ export class LspProcessSpawnError extends Error {
69
69
  override readonly name = "LspProcessSpawnError";
70
70
  }
71
71
 
72
+ export class LspClientRespawnBudgetExceededError extends Error {
73
+ override readonly name = "LspClientRespawnBudgetExceededError";
74
+
75
+ constructor(
76
+ readonly serverId: string,
77
+ readonly root: string,
78
+ readonly retryLimit: number,
79
+ ) {
80
+ super(
81
+ `LSP server ${serverId} at ${root} failed to stay alive; respawn budget exhausted ` +
82
+ `(${retryLimit} consecutive dead generations). Retrying after the cooldown may succeed.`,
83
+ );
84
+ }
85
+ }
86
+
72
87
  export function isLspDeadConnectionError(err: unknown): err is LspConnectionClosedError | LspProcessExitedError {
73
88
  return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
74
89
  }
@@ -8,6 +8,7 @@ let nextServerRequestId = 10_000;
8
8
  let renameIndex = 0;
9
9
  let diagnosticIndex = 0;
10
10
  const pendingServerRequests = new Map();
11
+ const releasedDiagnosticResponses = [];
11
12
 
12
13
  function record(event) {
13
14
  appendFileSync(eventsPath, `${JSON.stringify(event)}\n`, "utf-8");
@@ -146,8 +147,7 @@ function handleRequest(message) {
146
147
  setTimeout(() => process.exit(0), delay);
147
148
  return;
148
149
  }
149
- const delay = Number(step?.delayMs ?? 0);
150
- setTimeout(() => {
150
+ const sendDiagnosticResponse = () => {
151
151
  if (step?.error) {
152
152
  record({ type: "serverError", method: "textDocument/diagnostic", error: step.error });
153
153
  sendError(message.id, step.error.code, step.error.message);
@@ -156,7 +156,13 @@ function handleRequest(message) {
156
156
  const result = step?.report ?? { items: scenario.diagnostics ?? [] };
157
157
  record({ type: "serverResponse", method: "textDocument/diagnostic", result });
158
158
  sendResponse(message.id, result);
159
- }, delay);
159
+ };
160
+ if (step?.releaseOnDidChange === true) {
161
+ releasedDiagnosticResponses.push(sendDiagnosticResponse);
162
+ } else {
163
+ const delay = Number(step?.delayMs ?? 0);
164
+ setTimeout(sendDiagnosticResponse, delay);
165
+ }
160
166
  return;
161
167
  }
162
168
  if (message.method === "shutdown") {
@@ -190,6 +196,7 @@ function handleNotification(message) {
190
196
  }
191
197
  if (message.method === "textDocument/didChange") {
192
198
  applyPublishSteps("didChange", message.params.textDocument.uri, message.params.textDocument.version);
199
+ for (const release of releasedDiagnosticResponses.splice(0)) release();
193
200
  }
194
201
  if (message.method === "exit") process.exit(0);
195
202
  }
@@ -0,0 +1,262 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import type { LspClient } from "./client.js";
4
+ import { LspManager } from "./manager.js";
5
+ import type { ResolvedServer } from "./types.js";
6
+
7
+ const SERVER: ResolvedServer = {
8
+ id: "typescript",
9
+ command: ["typescript-language-server", "--stdio"],
10
+ extensions: [".ts"],
11
+ priority: 1,
12
+ };
13
+
14
+ /** Simulates a stop that lingers (production stops reach ~21s), which is the behavior under test. */
15
+ const SLOW_STOP_MS = 50;
16
+
17
+ interface RecordingClientOptions {
18
+ readonly stopDelayMs: number;
19
+ readonly initializeError?: Error;
20
+ readonly alive: boolean;
21
+ }
22
+
23
+ class RecordingFakeClient {
24
+ alive: boolean;
25
+ stopCallCount = 0;
26
+
27
+ constructor(
28
+ readonly root: string,
29
+ private readonly log: EventLog,
30
+ private readonly options: RecordingClientOptions,
31
+ ) {
32
+ this.alive = options.alive;
33
+ }
34
+
35
+ async start(): Promise<void> {
36
+ this.log.push(`spawn:${this.root}`);
37
+ }
38
+
39
+ async initialize(): Promise<void> {
40
+ if (this.options.initializeError !== undefined) {
41
+ throw this.options.initializeError;
42
+ }
43
+ }
44
+
45
+ isAlive(): boolean {
46
+ return this.alive;
47
+ }
48
+
49
+ command(): string[] {
50
+ return [...SERVER.command];
51
+ }
52
+
53
+ async stop(): Promise<void> {
54
+ this.stopCallCount += 1;
55
+ this.log.push(`stop-start:${this.root}`);
56
+ await delay(this.options.stopDelayMs);
57
+ this.alive = false;
58
+ this.log.push(`stop-end:${this.root}`);
59
+ }
60
+ }
61
+
62
+ class EventLog {
63
+ readonly entries: string[] = [];
64
+ private waiters: Array<{ readonly event: string; readonly resolve: () => void }> = [];
65
+
66
+ push(event: string): void {
67
+ this.entries.push(event);
68
+ const due = this.waiters.filter((waiter) => waiter.event === event);
69
+ if (due.length === 0) return;
70
+ this.waiters = this.waiters.filter((waiter) => waiter.event !== event);
71
+ for (const waiter of due) waiter.resolve();
72
+ }
73
+
74
+ waitFor(event: string): Promise<void> {
75
+ if (this.entries.includes(event)) return Promise.resolve();
76
+ return new Promise<void>((resolve) => {
77
+ this.waiters.push({ event, resolve });
78
+ });
79
+ }
80
+ }
81
+
82
+ function delay(ms: number): Promise<void> {
83
+ return new Promise((resolve) => setTimeout(resolve, ms));
84
+ }
85
+
86
+ interface HarnessOptions {
87
+ readonly maxResidentClients?: number;
88
+ readonly stopDelayMs?: number;
89
+ /** Leading generations whose initialize() rejects; later generations start cleanly. */
90
+ readonly initializeFailures?: number;
91
+ /** Generations at or above this index report alive; earlier ones emulate a crash-looping server. */
92
+ readonly aliveFromGeneration?: number;
93
+ }
94
+
95
+ function managerHarness(options: HarnessOptions = {}): {
96
+ readonly manager: LspManager;
97
+ readonly clients: RecordingFakeClient[];
98
+ readonly log: EventLog;
99
+ readonly clock: { value: number };
100
+ } {
101
+ const clients: RecordingFakeClient[] = [];
102
+ const log = new EventLog();
103
+ const clock = { value: 1_000 };
104
+ const generations = new Map<string, number>();
105
+ const aliveFrom = options.aliveFromGeneration ?? 1;
106
+ const stopDelayMs = options.stopDelayMs ?? 0;
107
+ const initializeFailures = options.initializeFailures ?? 0;
108
+ const manager = new LspManager({
109
+ maxResidentClients: options.maxResidentClients ?? 6,
110
+ reaperIntervalMs: 60_000,
111
+ idleTimeoutMs: 60_000_000,
112
+ now: () => clock.value,
113
+ clientFactory: (root) => {
114
+ const generation = (generations.get(root) ?? 0) + 1;
115
+ generations.set(root, generation);
116
+ const client = new RecordingFakeClient(root, log, {
117
+ stopDelayMs,
118
+ alive: generation >= aliveFrom,
119
+ ...(generation <= initializeFailures ? { initializeError: new Error("initialize boom") } : {}),
120
+ });
121
+ clients.push(client);
122
+ return client as unknown as LspClient;
123
+ },
124
+ });
125
+ return { manager, clients, log, clock };
126
+ }
127
+
128
+ describe("LspManager pending-stop tombstones", () => {
129
+ it("#given a slow-stopping client evicted at the resident cap #when the evicted root is re-acquired while the stop is pending #then the replacement spawns only after the old client stopped", async () => {
130
+ // given
131
+ const { manager, log } = managerHarness({ maxResidentClients: 1, stopDelayMs: SLOW_STOP_MS });
132
+ try {
133
+ await manager.getClient("/root-a", SERVER);
134
+ manager.releaseClient("/root-a", SERVER.id);
135
+
136
+ // when: admitting root-b synchronously evicts idle root-a (void stop), and root-a is
137
+ // re-acquired in the same window while the evicted client's stop is still pending.
138
+ const rootBPromise = manager.getClient("/root-b", SERVER);
139
+ const rootAPromise = manager.getClient("/root-a", SERVER);
140
+ await Promise.all([rootBPromise, rootAPromise]);
141
+ manager.releaseClient("/root-b", SERVER.id);
142
+ manager.releaseClient("/root-a", SERVER.id);
143
+
144
+ // then
145
+ expect(log.entries.filter((event) => event.endsWith("/root-a"))).toEqual([
146
+ "spawn:/root-a",
147
+ "stop-start:/root-a",
148
+ "stop-end:/root-a",
149
+ "spawn:/root-a",
150
+ ]);
151
+ } finally {
152
+ await manager.stopAll();
153
+ }
154
+ });
155
+
156
+ it("#given an invalidated client whose stop is still pending #when the same root is re-acquired #then the replacement spawns only after the invalidated client stopped", async () => {
157
+ // given
158
+ const { manager, log } = managerHarness({ stopDelayMs: SLOW_STOP_MS });
159
+ try {
160
+ await manager.getClient("/root-a", SERVER);
161
+ manager.releaseClient("/root-a", SERVER.id);
162
+
163
+ // when
164
+ manager.invalidateClient("/root-a", SERVER.id);
165
+ await manager.getClient("/root-a", SERVER);
166
+
167
+ // then
168
+ expect(log.entries).toEqual([
169
+ "spawn:/root-a",
170
+ "stop-start:/root-a",
171
+ "stop-end:/root-a",
172
+ "spawn:/root-a",
173
+ ]);
174
+ } finally {
175
+ await manager.stopAll();
176
+ }
177
+ });
178
+
179
+ it("#given a client whose initialization failed while its stop is pending #when another getClient races the stop #then the replacement spawns only after the failed client stopped", async () => {
180
+ // given
181
+ const { manager, log } = managerHarness({
182
+ stopDelayMs: SLOW_STOP_MS,
183
+ initializeFailures: 1,
184
+ });
185
+ try {
186
+ const failing = manager.getClient("/root-fail", SERVER);
187
+
188
+ // when: the second acquisition starts once the first one's stop is provably in flight.
189
+ await log.waitFor("stop-start:/root-fail");
190
+ const racing = manager.getClient("/root-fail", SERVER);
191
+ await expect(failing).rejects.toThrow("initialize boom");
192
+ await racing;
193
+ manager.releaseClient("/root-fail", SERVER.id);
194
+
195
+ // then
196
+ expect(log.entries).toEqual([
197
+ "spawn:/root-fail",
198
+ "stop-start:/root-fail",
199
+ "stop-end:/root-fail",
200
+ "spawn:/root-fail",
201
+ ]);
202
+ } finally {
203
+ await manager.stopAll();
204
+ }
205
+ });
206
+ });
207
+
208
+ describe("LspManager dead-client respawn budget", () => {
209
+ it("#given a crash-looping client that is dead on every acquisition #when getClient is called repeatedly #then respawn attempts stay within the retry budget", async () => {
210
+ // given
211
+ const { manager, clients } = managerHarness({ aliveFromGeneration: Number.POSITIVE_INFINITY });
212
+ try {
213
+ // when
214
+ for (let attempt = 0; attempt < 8; attempt += 1) {
215
+ await manager
216
+ .getClient("/root-crash", SERVER)
217
+ .then(
218
+ () => manager.releaseClient("/root-crash", SERVER.id),
219
+ () => undefined,
220
+ );
221
+ }
222
+
223
+ // then: one initial generation plus at most two budgeted respawns.
224
+ expect(clients.length).toBeLessThanOrEqual(3);
225
+ } finally {
226
+ await manager.stopAll();
227
+ }
228
+ });
229
+
230
+ it("#given the respawn budget is exhausted #when getClient is called again #then it rejects with a budget error instead of respawning", async () => {
231
+ // given
232
+ const { manager } = managerHarness({ aliveFromGeneration: Number.POSITIVE_INFINITY });
233
+ try {
234
+ await expect(manager.getClient("/root-crash", SERVER)).rejects.toThrow(/respawn budget/);
235
+
236
+ // when / then
237
+ await expect(manager.getClient("/root-crash", SERVER)).rejects.toThrow(/respawn budget/);
238
+ } finally {
239
+ await manager.stopAll();
240
+ }
241
+ });
242
+
243
+ it("#given the budget was exhausted but the cooldown elapsed and the server recovered #when getClient is called #then it spawns again and resets the budget on a healthy generation", async () => {
244
+ // given: generations one and two crash, the third is healthy.
245
+ const { manager, clients, clock } = managerHarness({ aliveFromGeneration: 3 });
246
+ try {
247
+ await expect(manager.getClient("/root-crash", SERVER)).rejects.toThrow(/respawn budget/);
248
+
249
+ // when: cooldown elapses (60s default) and the server stays alive again.
250
+ clock.value += 60_001;
251
+ const recovered = await manager.getClient("/root-crash", SERVER);
252
+ manager.releaseClient("/root-crash", SERVER.id);
253
+
254
+ // then
255
+ expect(clients.length).toBe(3);
256
+ await expect(manager.getClient("/root-crash", SERVER)).resolves.toBe(recovered);
257
+ manager.releaseClient("/root-crash", SERVER.id);
258
+ } finally {
259
+ await manager.stopAll();
260
+ }
261
+ });
262
+ });