@mrclrchtr/supi-code-intelligence 4.10.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +4 -3
  2. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  6. package/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  8. package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  9. package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  10. package/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  11. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
  14. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  16. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  17. package/node_modules/@mrclrchtr/supi-lsp/README.md +111 -5
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
  30. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  32. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
  34. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +22 -1
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +245 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +171 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +413 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +15 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +690 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +97 -16
  45. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
  46. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +448 -298
  47. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +70 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +231 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +529 -119
  51. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +161 -33
  52. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +12 -3
  53. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
  54. package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
  55. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
  56. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
  59. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +42 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
  62. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
  63. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +17 -7
  64. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +350 -26
  65. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
  66. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +586 -96
  67. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
  68. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
  69. package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
  70. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
  71. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
  72. package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
  73. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
  74. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +42 -0
  75. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +92 -0
  76. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +165 -118
  77. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
  78. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +17 -7
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +6 -3
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
  127. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
  128. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
  129. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
  130. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
  131. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
  132. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
  133. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
  134. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
  135. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
  136. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
  137. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
  138. package/package.json +5 -5
  139. package/src/analysis/health/diagnostics.ts +221 -77
  140. package/src/analysis/health/file-scope.ts +184 -0
  141. package/src/analysis/health/recovery.ts +14 -3
  142. package/src/analysis/provider.ts +58 -0
  143. package/src/analysis/readiness.ts +34 -9
  144. package/src/analysis/refactor/apply.ts +45 -1
  145. package/src/analysis/refactor/mutation-authority.ts +143 -0
  146. package/src/analysis/relations/callees.ts +9 -1
  147. package/src/analysis/relations/types.ts +5 -2
  148. package/src/analysis/search/ast-scan-timing.ts +9 -2
  149. package/src/analysis/search/ast-scan.ts +5 -2
  150. package/src/analysis/search/deadline.ts +2 -2
  151. package/src/analysis/search/pattern-analysis.ts +54 -33
  152. package/src/analysis/search/pattern.ts +18 -3
  153. package/src/analysis/target/anchored.ts +20 -11
  154. package/src/analysis/target/file.ts +18 -11
  155. package/src/analysis/target/identity.ts +19 -3
  156. package/src/analysis/target/symbol.ts +36 -15
  157. package/src/api.ts +4 -0
  158. package/src/config.ts +35 -1
  159. package/src/diagnostics/evidence.ts +51 -0
  160. package/src/extension.ts +57 -14
  161. package/src/overview/overview-data.ts +9 -2
  162. package/src/overview/overview.ts +49 -30
  163. package/src/overview/types.ts +2 -0
  164. package/src/session/capability-adapter.ts +12 -3
  165. package/src/session/find-types.ts +2 -2
  166. package/src/session/find-workflow.ts +20 -9
  167. package/src/session/graph/collect.ts +18 -2
  168. package/src/session/graph-workflow.ts +19 -12
  169. package/src/session/health-refresh.ts +175 -0
  170. package/src/session/health-types.ts +43 -15
  171. package/src/session/health-workflow.ts +69 -54
  172. package/src/session/input/workflows.ts +2 -2
  173. package/src/session/inspect/collect.ts +20 -3
  174. package/src/session/inspect-workflow.ts +12 -6
  175. package/src/session/orientation/collect.ts +12 -3
  176. package/src/session/orientation/context-facts.ts +3 -1
  177. package/src/session/orientation/context-sections.ts +20 -7
  178. package/src/session/orientation/gather.ts +17 -11
  179. package/src/session/orientation-types.ts +4 -1
  180. package/src/session/orientation-workflow.ts +23 -10
  181. package/src/session/refactor-plans.ts +2 -0
  182. package/src/session/refactor-workflow.ts +61 -15
  183. package/src/session/session.ts +29 -2
  184. package/src/session/target-workflow.ts +54 -23
  185. package/src/session/workflow-control.ts +7 -3
  186. package/src/substrate/lsp/lifecycle.ts +26 -46
  187. package/src/substrate/lsp/maintenance.ts +284 -28
  188. package/src/substrate/lsp/recovery.ts +11 -5
  189. package/src/substrate/lsp/state.ts +2 -2
  190. package/src/substrate/workspace-provider-host.ts +25 -3
  191. package/src/tool/{find → code_find}/execute.ts +2 -16
  192. package/src/tool/code_find/guidance.ts +8 -0
  193. package/src/tool/{find → code_find}/render.ts +21 -4
  194. package/src/tool/{result/find.ts → code_find/result.ts} +78 -9
  195. package/src/tool/code_find/spec.ts +37 -0
  196. package/src/tool/code_find/tui.ts +41 -0
  197. package/src/tool/code_graph/execute.ts +24 -0
  198. package/src/tool/code_graph/guidance.ts +6 -0
  199. package/src/tool/{graph → code_graph}/markdown.ts +1 -1
  200. package/src/tool/{result/graph.ts → code_graph/result.ts} +111 -3
  201. package/src/tool/code_graph/spec.ts +39 -0
  202. package/src/tool/code_graph/tui.ts +117 -0
  203. package/src/tool/{health → code_health}/execute.ts +2 -13
  204. package/src/tool/code_health/guidance.ts +8 -0
  205. package/src/tool/{health → code_health}/markdown.ts +119 -27
  206. package/src/tool/code_health/refresh-status.ts +105 -0
  207. package/src/tool/{result/health.ts → code_health/result.ts} +80 -3
  208. package/src/tool/code_health/spec.ts +41 -0
  209. package/src/tool/{health → code_health}/tui.ts +95 -98
  210. package/src/tool/{inspect → code_inspect}/execute.ts +2 -16
  211. package/src/tool/code_inspect/guidance.ts +6 -0
  212. package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
  213. package/src/tool/{result/inspect.ts → code_inspect/result.ts} +143 -2
  214. package/src/tool/code_inspect/spec.ts +25 -0
  215. package/src/tool/code_inspect/tui.ts +44 -0
  216. package/src/tool/code_orientation/execute.ts +25 -0
  217. package/src/tool/code_orientation/guidance.ts +9 -0
  218. package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
  219. package/src/tool/code_orientation/result.ts +248 -0
  220. package/src/tool/code_orientation/spec.ts +25 -0
  221. package/src/tool/{orientation → code_orientation}/tui.ts +51 -35
  222. package/src/tool/code_refactor_apply/execute.ts +17 -0
  223. package/src/tool/code_refactor_apply/guidance.ts +5 -0
  224. package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +8 -11
  225. package/src/tool/code_refactor_apply/spec.ts +26 -0
  226. package/src/tool/code_refactor_apply/tui.ts +73 -0
  227. package/src/tool/code_refactor_plan/execute.ts +27 -0
  228. package/src/tool/code_refactor_plan/guidance.ts +8 -0
  229. package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +13 -22
  230. package/src/tool/code_refactor_plan/spec.ts +25 -0
  231. package/src/tool/code_refactor_plan/tui.ts +54 -0
  232. package/src/tool/code_resolve/execute.ts +19 -0
  233. package/src/tool/code_resolve/guidance.ts +8 -0
  234. package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
  235. package/src/tool/{result/resolve.ts → code_resolve/result.ts} +79 -29
  236. package/src/tool/code_resolve/spec.ts +25 -0
  237. package/src/tool/code_resolve/tui.ts +60 -0
  238. package/src/tool/guidance.ts +71 -52
  239. package/src/tool/infra/truncate.ts +40 -0
  240. package/src/tool/infra/workflow-control.ts +8 -0
  241. package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
  242. package/src/tool/register.ts +105 -88
  243. package/src/tool/result/display.ts +107 -0
  244. package/src/tool/result/errors.ts +20 -17
  245. package/src/tool/result/refactor.ts +37 -8
  246. package/src/tool/result/types.ts +20 -0
  247. package/src/tool/schemas.ts +26 -157
  248. package/src/tool/specs.ts +40 -126
  249. package/src/types/execution.ts +18 -5
  250. package/src/types/index.ts +8 -1
  251. package/src/ui/status-command.ts +5 -3
  252. package/src/ui/status-overlay.ts +4 -0
  253. package/src/ui/tui/common.ts +108 -10
  254. package/src/ui/tui/display.ts +48 -0
  255. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -351
  256. package/src/tool/find/tui.ts +0 -39
  257. package/src/tool/graph/execute.ts +0 -62
  258. package/src/tool/graph/tui.ts +0 -104
  259. package/src/tool/inspect/tui.ts +0 -36
  260. package/src/tool/orientation/execute.ts +0 -62
  261. package/src/tool/refactor-apply/tui.ts +0 -55
  262. package/src/tool/refactor-plan/tui.ts +0 -41
  263. package/src/tool/resolve/execute.ts +0 -36
  264. package/src/tool/resolve/tui.ts +0 -42
  265. package/src/tool/result/orientation.ts +0 -132
  266. /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
  267. /package/src/tool/{find → code_find}/markdown.ts +0 -0
  268. /package/src/tool/{find → code_find}/modes.ts +0 -0
  269. /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
@@ -6,11 +6,14 @@ import { type ChildProcess, execSync, spawn } from "node:child_process";
6
6
  import * as path from "node:path";
7
7
  import {
8
8
  type CodeQueryResult,
9
+ type CodeRequestControl,
9
10
  completedCodeQuery,
11
+ isCodeRequestInterruption,
12
+ throwIfCodeRequestInterrupted,
10
13
  unavailableCodeQuery,
11
14
  } from "@mrclrchtr/supi-code-runtime/api";
12
15
  import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
13
- import type { ProgressToken } from "vscode-languageserver-protocol";
16
+ import { type ProgressToken, TextDocumentSyncKind } from "vscode-languageserver-protocol";
14
17
  import { CLIENT_CAPABILITIES } from "../config/capabilities.ts";
15
18
  import type {
16
19
  CodeAction,
@@ -25,7 +28,6 @@ import type {
25
28
  Location,
26
29
  LocationLink,
27
30
  Position,
28
- PublishDiagnosticsParams,
29
31
  Range,
30
32
  ServerCapabilities,
31
33
  ServerConfig,
@@ -33,14 +35,35 @@ import type {
33
35
  WorkspaceEdit,
34
36
  WorkspaceSymbol,
35
37
  } from "../config/types.ts";
38
+ import { boundCwd, truncateIdentity } from "../debug-telemetry.ts";
39
+ import type { DiagnosticEvidenceSummary } from "../diagnostics/evidence.ts";
40
+ import { raceRequestControl } from "../session/readiness.ts";
36
41
  import { fileToUri } from "../utils.ts";
37
- import { ClientDiagnostics, type DiagnosticEntry } from "./client-diagnostics.ts";
42
+ import {
43
+ ClientDynamicRegistrations,
44
+ DOCUMENT_DIAGNOSTIC_METHOD,
45
+ isValidDiagnosticOptions,
46
+ } from "./client-diagnostic-capabilities.ts";
47
+ import { ClientDiagnostics } from "./client-diagnostics.ts";
48
+ import type { ClientDiagnosticSnapshot, DiagnosticEntry } from "./client-document-state.ts";
38
49
  import { JsonRpcClient, JsonRpcRequestError } from "./transport.ts";
39
50
 
40
51
  const SHUTDOWN_TIMEOUT_MS = 5_000;
41
52
 
53
+ /**
54
+ * Fixed bound after which a running client that never became ready is
55
+ * considered readiness-stalled and eligible for a recovery restart.
56
+ */
57
+ export const RECOVERY_CLIENT_STARTUP_BOUND_MS = 5_000;
58
+
59
+ /** Repeated protocol-stall failures that justify a recovery restart. */
60
+ export const RECOVERY_PROTOCOL_FAILURE_THRESHOLD = 3;
61
+
62
+ /** Stall signals that justify replacing a client's server process. */
63
+ export type RecoveryRestartReason = "readiness-stall" | "protocol-errors";
64
+
42
65
  /** Race an operation against a timeout without retaining the timer after settlement. */
43
- async function withTimeout<T>(
66
+ export async function withTimeout<T>(
44
67
  operation: Promise<T>,
45
68
  timeoutMs: number,
46
69
  message: string,
@@ -58,6 +81,41 @@ async function withTimeout<T>(
58
81
  }
59
82
  }
60
83
 
84
+ function isRecord(value: unknown): value is Record<string, unknown> {
85
+ return typeof value === "object" && value !== null && !Array.isArray(value);
86
+ }
87
+
88
+ /**
89
+ * Read and validate the `registrations` array of a `client/registerCapability`
90
+ * request. Each entry must be a record with string `id` and `method`; the
91
+ * optional `registerOptions` stay unvalidated here (method-specific checks
92
+ * happen in the handler). Malformed values reject the request.
93
+ */
94
+ function readRegistrations(
95
+ params: unknown,
96
+ requestName: string,
97
+ ): Array<{ id: string; method: string; registerOptions?: unknown }> {
98
+ if (!isRecord(params) || !Array.isArray(params.registrations)) {
99
+ throw new JsonRpcRequestError(-32602, `Malformed ${requestName} params.`);
100
+ }
101
+ const registrations: Array<{ id: string; method: string; registerOptions?: unknown }> = [];
102
+ for (const registration of params.registrations) {
103
+ if (
104
+ !isRecord(registration) ||
105
+ typeof registration.id !== "string" ||
106
+ typeof registration.method !== "string"
107
+ ) {
108
+ throw new JsonRpcRequestError(-32602, `Malformed ${requestName} registration.`);
109
+ }
110
+ registrations.push({
111
+ id: registration.id,
112
+ method: registration.method,
113
+ registerOptions: registration.registerOptions,
114
+ });
115
+ }
116
+ return registrations;
117
+ }
118
+
61
119
  // ── Process-tree cleanup ──────────────────────────────────────────────
62
120
 
63
121
  /**
@@ -86,6 +144,17 @@ function killProcessTree(pid: number): void {
86
144
  // ── Types ─────────────────────────────────────────────────────────────
87
145
  export type ClientStatus = "initializing" | "running" | "error" | "shutdown";
88
146
 
147
+ /** Package-internal facts that the manager projects into workspace lifecycle transitions. */
148
+ export type LspClientLifecycleTransitionKind =
149
+ | "startup"
150
+ | "readiness"
151
+ | "crash"
152
+ | "shutdown"
153
+ | "tracked-files";
154
+
155
+ /** Observer for one concrete client's lifecycle facts. */
156
+ export type LspClientLifecycleListener = (kind: LspClientLifecycleTransitionKind) => void;
157
+
89
158
  // ── LspClient ─────────────────────────────────────────────────────────
90
159
  export class LspClient {
91
160
  readonly name: string;
@@ -96,37 +165,62 @@ export class LspClient {
96
165
  private _status: ClientStatus = "initializing";
97
166
  private capabilities: ServerCapabilities | null = null;
98
167
  private readonly diagnostics: ClientDiagnostics;
168
+ /** Dynamic capability registrations for this client instance only. */
169
+ private readonly dynamicRegistrations = new ClientDynamicRegistrations();
99
170
 
100
171
  // ── Readiness (work-done-progress) ──────────────────────────────────
101
- private trackedTokens = new Map<ProgressToken, "begin-seen" | "ended">();
172
+ private trackedTokens = new Map<ProgressToken, "created" | "active" | "ended">();
173
+ private tokenCreatedAt = new Map<ProgressToken, number>();
102
174
  private _readyPromise: Promise<void> | null = null;
103
175
  private _readyResolve: (() => void) | undefined;
104
176
  private _readyReject: ((err: Error) => void) | undefined;
105
177
  private _isReady = false;
178
+ /** Whether this client generation ever reached concrete readiness. */
179
+ private everReady = false;
106
180
  private noProgressTimer: ReturnType<typeof setTimeout> | null = null;
107
181
  private tokenTimeouts = new Map<ProgressToken, ReturnType<typeof setTimeout>>();
182
+ /** Wall-clock start of the current client generation, for stall detection. */
183
+ private startedAt = 0;
108
184
 
185
+ // biome-ignore lint/complexity/useMaxParams: internal constructor keeps positional identity for test call sites
109
186
  constructor(
110
187
  name: string,
111
188
  private readonly config: ServerConfig,
112
189
  root: string,
190
+ private readonly onLifecycleTransition?: LspClientLifecycleListener,
191
+ /** Absolute workspace root for debug-telemetry identity. */
192
+ readonly cwd?: string,
113
193
  ) {
114
194
  this.name = name;
115
195
  this.root = root;
116
196
  this.diagnostics = new ClientDiagnostics({
197
+ server: name,
198
+ cwd: cwd,
117
199
  isOperational: () => this.rpc !== null && this._status === "running",
118
200
  supportsPullDiagnostics: () => this.hasDiagnosticProvider,
201
+ usesIncrementalDocumentSync: () => this.usesIncrementalDocumentSync,
119
202
  sendNotification: (method, params) => {
120
203
  if (this.rpc) void this.rpc.sendNotification(method, params);
121
204
  },
122
- pullDocumentDiagnostics: async (uri, previousResultId, timeoutMs) => {
205
+ pullDocumentDiagnostics: async (request) => {
123
206
  const rpc = this.rpc;
124
207
  if (!rpc || this._status !== "running") throw new Error("client not running");
125
- await this.getReady();
208
+ await this.getReady({
209
+ signal: request.signal,
210
+ deadline: request.deadline,
211
+ });
126
212
  return rpc.sendRequest(
127
- "textDocument/diagnostic",
128
- { textDocument: { uri }, previousResultId },
129
- { timeoutMs },
213
+ DOCUMENT_DIAGNOSTIC_METHOD,
214
+ {
215
+ textDocument: { uri: request.uri },
216
+ previousResultId: request.previousResultId,
217
+ },
218
+ {
219
+ timeoutMs: request.timeoutMs,
220
+ signal: request.signal,
221
+ deadline: request.deadline,
222
+ operationId: request.operationId,
223
+ },
130
224
  ) as Promise<DocumentDiagnosticReport>;
131
225
  },
132
226
  });
@@ -144,44 +238,66 @@ export class LspClient {
144
238
  return this.capabilities;
145
239
  }
146
240
 
241
+ /** Whether the server requires range-based document changes. */
242
+ get usesIncrementalDocumentSync(): boolean {
243
+ const sync = this.capabilities?.textDocumentSync;
244
+ const change = typeof sync === "number" ? sync : sync?.change;
245
+ return change === TextDocumentSyncKind.Incremental;
246
+ }
247
+
147
248
  /** Whether the server is currently not indexing and ready to serve queries. */
148
249
  get ready(): boolean {
149
250
  return this._isReady;
150
251
  }
151
252
 
253
+ /** Publish one client fact without letting an observer disrupt the client. */
254
+ private publishLifecycle(kind: LspClientLifecycleTransitionKind): void {
255
+ try {
256
+ this.onLifecycleTransition?.(kind);
257
+ } catch {
258
+ // Lifecycle observers must not alter protocol behavior.
259
+ }
260
+ }
261
+
152
262
  // ── Lifecycle ───────────────────────────────────────────────────────
153
263
  /** Spawn the server process and perform the initialize handshake. */
154
264
  async start(): Promise<void> {
155
265
  const cmd = this.config.command;
156
266
  const args = this.config.args ?? [];
267
+ this.startedAt = Date.now();
157
268
 
158
269
  try {
159
270
  this.process = spawn(cmd, args, {
160
271
  cwd: this.root,
161
272
  stdio: ["pipe", "pipe", "pipe"],
162
- env: { ...process.env },
273
+ env: { ...process.env, ...this.config.env },
163
274
  // Run the server in its own process group so we can atomically
164
275
  // kill the entire tree (server + subprocesses like tsserver)
165
276
  // with `process.kill(-pid, signal)` on Unix or `taskkill /T` on Windows.
166
277
  detached: true,
167
278
  });
168
279
  } catch (err) {
169
- this._status = "error";
170
- throw new Error(`Failed to spawn ${cmd}: ${err}`, { cause: err });
280
+ const failure = new Error(`Failed to spawn ${cmd}: ${err}`, { cause: err });
281
+ this.handleProcessFailure(failure);
282
+ throw failure;
171
283
  }
172
284
 
173
285
  if (!this.process.stdin || !this.process.stdout) {
174
- this._status = "error";
286
+ const failure = new Error(`${cmd}: missing stdin/stdout`);
287
+ this.handleProcessFailure(failure);
175
288
  this.process.kill();
176
- throw new Error(`${cmd}: missing stdin/stdout`);
289
+ throw failure;
177
290
  }
178
291
 
179
- this.rpc = new JsonRpcClient(this.process.stdout, this.process.stdin);
292
+ this.rpc = new JsonRpcClient(this.process.stdout, this.process.stdin, {
293
+ server: this.name,
294
+ cwd: this.cwd,
295
+ });
180
296
 
181
297
  // Handle notifications
182
298
  this.rpc.onNotification((method, params) => {
183
299
  if (method === "textDocument/publishDiagnostics") {
184
- this.handlePublishDiagnostics(params as PublishDiagnosticsParams);
300
+ this.handlePublishDiagnostics(params);
185
301
  } else if (method === "$/progress") {
186
302
  this.handleProgress(params as { token: ProgressToken; value: { kind: string } });
187
303
  }
@@ -190,19 +306,11 @@ export class LspClient {
190
306
 
191
307
  // Handle crashes
192
308
  this.process.on("exit", (_code) => {
193
- if (this._status !== "shutdown") {
194
- this._status = "error";
195
- this.cancelNoProgressTimer();
196
- this.rejectReady(new Error("Client crashed"));
197
- }
198
- this.rpc?.dispose();
309
+ this.handleProcessFailure(new Error("Client crashed"));
199
310
  });
200
311
 
201
312
  this.process.on("error", (_err) => {
202
- if (this._status !== "shutdown") {
203
- this._status = "error";
204
- this.rejectReady(new Error("Client process error"));
205
- }
313
+ this.handleProcessFailure(new Error("Client process error"));
206
314
  });
207
315
 
208
316
  // Suppress stderr to avoid noise in the agent
@@ -213,19 +321,31 @@ export class LspClient {
213
321
  const result = (await this.rpc.sendRequest("initialize", {
214
322
  processId: process.pid,
215
323
  rootUri: fileToUri(this.root),
324
+ workspaceFolders: [
325
+ {
326
+ uri: fileToUri(this.root),
327
+ name: path.basename(this.root) || this.root,
328
+ },
329
+ ],
216
330
  capabilities: CLIENT_CAPABILITIES,
217
331
  initializationOptions: this.config.initializationOptions,
218
332
  })) as InitializeResult;
219
333
 
334
+ const positionEncoding = result.capabilities.positionEncoding ?? "utf-16";
335
+ if (positionEncoding !== "utf-16") {
336
+ throw new Error(`Server selected unsupported position encoding "${positionEncoding}".`);
337
+ }
220
338
  this.capabilities = result.capabilities;
221
- void this.rpc.sendNotification("initialized", {});
339
+ await this.rpc.sendNotification("initialized", {});
222
340
  this._status = "running";
341
+ this.publishLifecycle("startup");
223
342
 
224
343
  this.armNoProgressTimer();
225
344
  } catch (err) {
226
- this._status = "error";
345
+ const failure = new Error(`${this.name}: initialize failed: ${err}`, { cause: err });
346
+ this.handleProcessFailure(failure);
227
347
  this.process.kill();
228
- throw new Error(`${this.name}: initialize failed: ${err}`, { cause: err });
348
+ throw failure;
229
349
  }
230
350
  }
231
351
 
@@ -233,6 +353,11 @@ export class LspClient {
233
353
  async shutdown(): Promise<void> {
234
354
  if (this._status === "shutdown") return;
235
355
  this._status = "shutdown";
356
+ this.diagnostics.clear();
357
+ this.dynamicRegistrations.clear();
358
+ this.cancelNoProgressTimer();
359
+ this.rejectReady(new Error("Client shutdown"));
360
+ this.publishLifecycle("shutdown");
236
361
 
237
362
  if (!this.rpc || !this.process) return;
238
363
 
@@ -271,38 +396,123 @@ export class LspClient {
271
396
  });
272
397
  }
273
398
  }
399
+ }
274
400
 
275
- this.diagnostics.clear();
401
+ /**
402
+ * Terminate the process tree without a protocol handshake.
403
+ *
404
+ * Used when a replacement startup exceeds its recovery bound, so the
405
+ * orphaned server process cannot outlive the failed restart.
406
+ */
407
+ async forceKill(): Promise<void> {
408
+ const pid = this.process?.pid;
409
+ this.rpc?.dispose();
410
+ if (pid && this.process?.exitCode === null) {
411
+ killProcessTree(pid);
412
+ if (process.platform !== "win32") {
413
+ // Escalate to SIGKILL after a brief grace period on Unix, mirroring
414
+ // the graceful shutdown path for servers that ignore SIGTERM.
415
+ await new Promise<void>((resolve) => {
416
+ setTimeout(() => {
417
+ try {
418
+ process.kill(-pid, "SIGKILL");
419
+ } catch {
420
+ // Already dead — ignore.
421
+ }
422
+ resolve();
423
+ }, 500);
424
+ });
425
+ }
426
+ }
427
+ this.handleProcessFailure(new Error("Client start bound exceeded"));
428
+ }
276
429
 
277
- // Clear readiness state
278
- if (this.noProgressTimer) {
279
- clearTimeout(this.noProgressTimer);
280
- this.noProgressTimer = null;
430
+ private handleProcessFailure(reason: Error): void {
431
+ const didCrash = this._status !== "shutdown" && this._status !== "error";
432
+ if (didCrash) {
433
+ this._status = "error";
434
+ this.cancelNoProgressTimer();
435
+ this.rejectReady(reason);
281
436
  }
282
- for (const timer of this.tokenTimeouts.values()) clearTimeout(timer);
283
- this.tokenTimeouts.clear();
284
- this.rejectReady(new Error("Client shutdown"));
437
+ this.diagnostics.clear({ preserveFailedDocuments: didCrash || this._status === "error" });
438
+ this.dynamicRegistrations.clear();
439
+ this.rpc?.dispose();
440
+ if (didCrash) this.publishLifecycle("crash");
285
441
  }
286
442
 
287
443
  // ── Document Synchronization and Diagnostics ────────────────────────
288
444
  /** Open a document, or update it when it is already open. */
289
445
  didOpen(filePath: string, content: string): void {
446
+ const trackedCount = this.openFiles.length;
290
447
  this.diagnostics.didOpen(filePath, content);
448
+ this.publishTrackedFileChange(trackedCount);
291
449
  }
292
450
 
293
451
  /** Update a document, or open it when it is not tracked yet. */
294
452
  didChange(filePath: string, content: string): void {
453
+ const trackedCount = this.openFiles.length;
295
454
  this.diagnostics.didChange(filePath, content);
455
+ this.publishTrackedFileChange(trackedCount);
296
456
  }
297
457
 
298
458
  /** Close a document and remove its cached diagnostic state. */
299
459
  didClose(filePath: string): void {
460
+ const trackedCount = this.openFiles.length;
300
461
  this.diagnostics.didClose(filePath);
462
+ this.publishTrackedFileChange(trackedCount);
301
463
  }
302
464
 
303
465
  /** Remove missing document and diagnostic state, and return the removed paths. */
304
466
  pruneMissingFiles(): string[] {
305
- return this.diagnostics.pruneMissingFiles();
467
+ const trackedCount = this.openFiles.length;
468
+ const removed = this.diagnostics.pruneMissingFiles();
469
+ this.publishTrackedFileChange(trackedCount);
470
+ return removed;
471
+ }
472
+
473
+ private publishTrackedFileChange(previousCount: number): void {
474
+ if (this.openFiles.length !== previousCount) this.publishLifecycle("tracked-files");
475
+ }
476
+
477
+ /** Retain a failed document outcome when a replacement cannot reopen it. */
478
+ markFailedFile(filePath: string): void {
479
+ this.diagnostics.markFailedFile(filePath);
480
+ }
481
+
482
+ /**
483
+ * Return the stall signal that justifies replacing this client's process,
484
+ * or null when the client is healthy. Recovery restarts clients only on
485
+ * these signals, never on unconfirmed evidence alone (ADR 0020).
486
+ */
487
+ getRecoveryStallSignal(): RecoveryRestartReason | null {
488
+ if (this._status !== "running") return null;
489
+ // The startup bound applies only before the client ever became ready: a
490
+ // later readiness loss (a normal progress begin during indexing) is not
491
+ // a startup stall.
492
+ const pastStartupBound =
493
+ this.startedAt > 0 && Date.now() - this.startedAt >= RECOVERY_CLIENT_STARTUP_BOUND_MS;
494
+ if (!this.everReady && pastStartupBound) return "readiness-stall";
495
+ if (this.hasUnbegunCreatedToken()) return "readiness-stall";
496
+ if ((this.rpc?.getProtocolFailureCount() ?? 0) >= RECOVERY_PROTOCOL_FAILURE_THRESHOLD) {
497
+ return "protocol-errors";
498
+ }
499
+ return null;
500
+ }
501
+
502
+ /** Test whether a created progress token never began within its per-token bound. */
503
+ private hasUnbegunCreatedToken(): boolean {
504
+ const boundMs = this.config.readinessTimeoutMs ?? 10_000;
505
+ for (const [token, createdAt] of this.tokenCreatedAt) {
506
+ if (this.trackedTokens.get(token) === "created" && Date.now() - createdAt >= boundMs) {
507
+ return true;
508
+ }
509
+ }
510
+ return false;
511
+ }
512
+
513
+ /** Return the current client version, or null when the document is not open. */
514
+ getOpenDocumentVersion(filePath: string): number | null {
515
+ return this.diagnostics.getOpenDocumentVersion(filePath);
306
516
  }
307
517
 
308
518
  /** Return stored diagnostics for one file. */
@@ -311,8 +521,12 @@ export class LspClient {
311
521
  }
312
522
 
313
523
  /** Return non-empty diagnostics for files that still exist. */
524
+ getDiagnosticSnapshot(): ClientDiagnosticSnapshot {
525
+ return this.diagnostics.getDiagnosticSnapshot();
526
+ }
527
+
314
528
  getAllDiagnostics(): DiagnosticEntry[] {
315
- return this.diagnostics.getAllDiagnostics();
529
+ return this.getDiagnosticSnapshot().entries;
316
530
  }
317
531
 
318
532
  /** Force the next pull refresh to request complete diagnostic reports. */
@@ -322,12 +536,20 @@ export class LspClient {
322
536
 
323
537
  /** Check if server supports pull diagnostics. */
324
538
  get hasDiagnosticProvider(): boolean {
325
- return this.capabilities?.diagnosticProvider !== undefined;
539
+ // Static state: a valid `diagnosticProvider` in the initialize result.
540
+ // Dynamic state: an active registration for the diagnostic method. A
541
+ // malformed static shape or an empty dynamic set fails closed, so an
542
+ // unsupported server never gets pull requests.
543
+ return (
544
+ isValidDiagnosticOptions(this.capabilities?.diagnosticProvider) ||
545
+ this.dynamicRegistrations.has(DOCUMENT_DIAGNOSTIC_METHOD)
546
+ );
326
547
  }
327
548
 
328
549
  /** Notify the server that watched workspace files changed. */
329
550
  notifyWorkspaceFileChanges(changes: FileEvent[]): void {
330
551
  if (!this.rpc || this._status !== "running" || changes.length === 0) return;
552
+ this.diagnostics.invalidateCachedEvidence();
331
553
  void this.rpc.sendNotification("workspace/didChangeWatchedFiles", {
332
554
  changes,
333
555
  } satisfies DidChangeWatchedFilesParams);
@@ -335,118 +557,157 @@ export class LspClient {
335
557
 
336
558
  /** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
337
559
  async refreshOpenDiagnostics(
338
- options: { maxWaitMs?: number; quietMs?: number } = {},
339
- ): Promise<void> {
560
+ options: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl = {},
561
+ ): Promise<DiagnosticEvidenceSummary> {
340
562
  return this.diagnostics.refreshOpenDiagnostics(options);
341
563
  }
342
564
 
343
- /** Sync one file and return its diagnostics after pull or push collection. */
344
- async syncAndWaitForDiagnostics(filePath: string, content: string): Promise<Diagnostic[]> {
345
- return this.diagnostics.syncAndWaitForDiagnostics(filePath, content);
565
+ /** Sync one file and return diagnostics with explicit evidence availability. */
566
+ async syncAndWaitForDiagnostics(
567
+ filePath: string,
568
+ content: string,
569
+ control?: CodeRequestControl,
570
+ ): Promise<CodeQueryResult<Diagnostic[]>> {
571
+ return this.diagnostics.syncAndWaitForDiagnostics(filePath, content, control);
346
572
  }
347
573
 
348
574
  // ── LSP Requests ───────────────────────────────────────────────────
349
- async hover(filePath: string, position: Position): Promise<CodeQueryResult<Hover | null>> {
350
- return this.query("textDocument/hover", {
351
- textDocument: { uri: fileToUri(filePath) },
352
- position,
353
- });
575
+ async hover(
576
+ filePath: string,
577
+ position: Position,
578
+ control?: CodeRequestControl,
579
+ ): Promise<CodeQueryResult<Hover | null>> {
580
+ return this.query(
581
+ "textDocument/hover",
582
+ { textDocument: { uri: fileToUri(filePath) }, position },
583
+ control,
584
+ );
354
585
  }
355
586
 
356
587
  async definition(
357
588
  filePath: string,
358
589
  position: Position,
590
+ control?: CodeRequestControl,
359
591
  ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
360
- return this.query("textDocument/definition", {
361
- textDocument: { uri: fileToUri(filePath) },
362
- position,
363
- });
592
+ return this.query(
593
+ "textDocument/definition",
594
+ { textDocument: { uri: fileToUri(filePath) }, position },
595
+ control,
596
+ );
364
597
  }
365
598
 
366
599
  async references(
367
600
  filePath: string,
368
601
  position: Position,
602
+ control?: CodeRequestControl,
369
603
  ): Promise<CodeQueryResult<Location[] | null>> {
370
- return this.query("textDocument/references", {
371
- textDocument: { uri: fileToUri(filePath) },
372
- position,
373
- context: { includeDeclaration: true },
374
- });
604
+ return this.query(
605
+ "textDocument/references",
606
+ {
607
+ textDocument: { uri: fileToUri(filePath) },
608
+ position,
609
+ context: { includeDeclaration: true },
610
+ },
611
+ control,
612
+ );
375
613
  }
376
614
 
377
615
  async documentSymbols(
378
616
  filePath: string,
617
+ control?: CodeRequestControl,
379
618
  ): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[] | null>> {
380
- return this.query("textDocument/documentSymbol", {
381
- textDocument: { uri: fileToUri(filePath) },
382
- });
619
+ return this.query(
620
+ "textDocument/documentSymbol",
621
+ { textDocument: { uri: fileToUri(filePath) } },
622
+ control,
623
+ );
383
624
  }
384
625
 
385
626
  async workspaceSymbol(
386
627
  query: string,
628
+ control?: CodeRequestControl,
387
629
  ): Promise<CodeQueryResult<SymbolInformation[] | WorkspaceSymbol[] | null>> {
388
630
  if (!this.capabilities?.workspaceSymbolProvider) {
389
631
  return unavailableCodeQuery("Workspace-symbol requests are not supported by this server.");
390
632
  }
391
- return this.query("workspace/symbol", { query });
633
+ return this.query("workspace/symbol", { query }, control);
392
634
  }
393
635
 
394
636
  async rename(
395
637
  filePath: string,
396
638
  position: Position,
397
639
  newName: string,
640
+ control?: CodeRequestControl,
398
641
  ): Promise<WorkspaceEdit | null> {
399
- return this.request("textDocument/rename", {
400
- textDocument: { uri: fileToUri(filePath) },
401
- position,
402
- newName,
403
- });
642
+ return this.request(
643
+ "textDocument/rename",
644
+ { textDocument: { uri: fileToUri(filePath) }, position, newName },
645
+ control,
646
+ );
404
647
  }
405
648
 
406
649
  async codeActions(
407
650
  filePath: string,
408
651
  range: Range,
409
652
  context: CodeActionContext,
653
+ control?: CodeRequestControl,
410
654
  ): Promise<CodeAction[] | null> {
411
- return this.request("textDocument/codeAction", {
412
- textDocument: { uri: fileToUri(filePath) },
413
- range,
414
- context,
415
- });
655
+ return this.request(
656
+ "textDocument/codeAction",
657
+ { textDocument: { uri: fileToUri(filePath) }, range, context },
658
+ control,
659
+ );
416
660
  }
417
661
 
418
662
  async implementation(
419
663
  filePath: string,
420
664
  position: Position,
665
+ control?: CodeRequestControl,
421
666
  ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
422
667
  if (!this.capabilities?.implementationProvider) {
423
668
  return unavailableCodeQuery("Implementation requests are not supported by this server.");
424
669
  }
425
- return this.query("textDocument/implementation", {
426
- textDocument: { uri: fileToUri(filePath) },
427
- position,
428
- });
670
+ return this.query(
671
+ "textDocument/implementation",
672
+ { textDocument: { uri: fileToUri(filePath) }, position },
673
+ control,
674
+ );
429
675
  }
430
676
 
431
677
  // ── Private ─────────────────────────────────────────────────────────
432
- private async query<T>(method: string, params: unknown): Promise<CodeQueryResult<T | null>> {
678
+ private async query<T>(
679
+ method: string,
680
+ params: unknown,
681
+ control?: CodeRequestControl,
682
+ ): Promise<CodeQueryResult<T | null>> {
683
+ // An already-cancelled caller gets the interruption, not an unavailable
684
+ // outcome that could mask the cancellation.
685
+ throwIfCodeRequestInterrupted(control);
433
686
  if (!this.rpc || this._status !== "running") {
434
687
  return unavailableCodeQuery(
435
688
  `LSP request ${method} is unavailable because the client is not running.`,
436
689
  );
437
690
  }
438
691
  try {
439
- await this.getReady();
440
- const data = (await this.rpc.sendRequest(method, params)) as T | null | undefined;
692
+ await this.getReady(control);
693
+ const data = (await this.rpc.sendRequest(method, params, control)) as T | null | undefined;
441
694
  return completedCodeQuery(data ?? null);
442
695
  } catch (error) {
696
+ // Cancellation and absolute-deadline expiry propagate as interruptions:
697
+ // the caller no longer awaits a result, so no unavailable outcome may
698
+ // mask the cancellation.
699
+ if (isCodeRequestInterruption(error, control)) throw error;
443
700
  const detail = error instanceof Error ? error.message : String(error);
444
701
  return unavailableCodeQuery(`LSP request ${method} failed: ${detail}`);
445
702
  }
446
703
  }
447
704
 
448
- private async request<T>(method: string, params: unknown): Promise<T | null> {
449
- const result = await this.query<T>(method, params);
705
+ private async request<T>(
706
+ method: string,
707
+ params: unknown,
708
+ control?: CodeRequestControl,
709
+ ): Promise<T | null> {
710
+ const result = await this.query<T>(method, params, control);
450
711
  return result.kind === "unavailable" ? null : result.data;
451
712
  }
452
713
 
@@ -457,23 +718,21 @@ export class LspClient {
457
718
  case "workspace/workspaceFolders":
458
719
  return [{ uri: fileToUri(this.root), name: path.basename(this.root) || this.root }];
459
720
  case "client/registerCapability":
721
+ return this.handleRegisterCapability(params);
460
722
  case "client/unregisterCapability":
461
- return null;
723
+ return this.handleUnregisterCapability(params);
724
+ case "workspace/diagnostic/refresh":
725
+ // Refresh the tracked diagnostic set without blocking the server's
726
+ // request. The LSP response is always null; the pass records its
727
+ // terminal result asynchronously for local protocol diagnosis.
728
+ return this.handleServerDiagnosticRefreshRequest();
462
729
  case "window/workDoneProgress/create": {
730
+ // A create reserves a token; it does not prove active work. The
731
+ // token stays pending and readiness is untouched until a begin
732
+ // arrives, so an unused token never causes false readiness loss.
463
733
  const token = (params as { token: ProgressToken }).token;
464
- this.trackedTokens.set(token, "begin-seen");
465
- this.cancelNoProgressTimer();
466
- this._isReady = false;
467
- if (!this._readyPromise) {
468
- this._readyPromise = new Promise<void>((resolve, reject) => {
469
- this._readyResolve = resolve;
470
- this._readyReject = reject;
471
- });
472
- // Prevent unhandled rejection when rejectReady fires before any
473
- // consumer is actively awaiting this promise (e.g. during shutdown).
474
- this._readyPromise.catch(() => {});
475
- }
476
- this.startTokenTimeout(token);
734
+ this.trackedTokens.set(token, "created");
735
+ this.tokenCreatedAt.set(token, Date.now());
477
736
  return null;
478
737
  }
479
738
  default:
@@ -481,6 +740,51 @@ export class LspClient {
481
740
  }
482
741
  }
483
742
 
743
+ /** Run a server-requested refresh through the forced diagnostic path. */
744
+ private refreshForServerRequest(): Promise<DiagnosticEvidenceSummary> {
745
+ return this.diagnostics.refreshForServerRequest();
746
+ }
747
+
748
+ private handleServerDiagnosticRefreshRequest(): null {
749
+ // Defer the pass before invoking it. Its setup rereads and resynchronizes
750
+ // tracked documents, so even an async method can otherwise delay the null
751
+ // response on the JSON-RPC request stack.
752
+ void Promise.resolve()
753
+ .then(() => this.refreshForServerRequest())
754
+ .then(
755
+ (evidence) => this.recordDiagnosticRefreshRequest("completed", evidence),
756
+ () => this.recordDiagnosticRefreshRequest("failed"),
757
+ )
758
+ // Consume failures from the telemetry callback as well as the pass.
759
+ .catch(() => {});
760
+ return null;
761
+ }
762
+
763
+ private recordDiagnosticRefreshRequest(
764
+ outcome: "completed" | "failed",
765
+ evidence?: DiagnosticEvidenceSummary,
766
+ ): void {
767
+ recordDebugEvent({
768
+ source: "lsp",
769
+ level: "debug",
770
+ category: "diagnostics.refresh-request",
771
+ message: `LSP diagnostic refresh request ${outcome}`,
772
+ cwd: boundCwd(this.cwd),
773
+ data:
774
+ outcome === "completed" && evidence
775
+ ? {
776
+ outcome,
777
+ server: truncateIdentity(this.name),
778
+ requested: evidence.requested,
779
+ confirmed: evidence.confirmed,
780
+ unconfirmed: evidence.unconfirmed,
781
+ failed: evidence.failed,
782
+ removed: evidence.removed,
783
+ }
784
+ : { outcome, server: truncateIdentity(this.name) },
785
+ });
786
+ }
787
+
484
788
  private buildWorkspaceConfigurationResult(params: unknown): unknown[] {
485
789
  if (!params || typeof params !== "object") return [];
486
790
  const items = (params as { items?: unknown }).items;
@@ -488,8 +792,52 @@ export class LspClient {
488
792
  return items.map(() => null);
489
793
  }
490
794
 
795
+ /**
796
+ * Apply a dynamic registration for `textDocument/diagnostic`.
797
+ *
798
+ * Registrations for other methods are ignored (status quo). Malformed
799
+ * params or malformed diagnostic registration options reject the request
800
+ * without enabling pull, so a server never gets pull requests it did not
801
+ * validly register for.
802
+ */
803
+ private handleRegisterCapability(params: unknown): null {
804
+ const registrations = readRegistrations(params, "client/registerCapability");
805
+ for (const registration of registrations) {
806
+ if (registration.method !== DOCUMENT_DIAGNOSTIC_METHOD) continue;
807
+ if (!isValidDiagnosticOptions(registration.registerOptions)) {
808
+ throw new JsonRpcRequestError(
809
+ -32602,
810
+ "Malformed textDocument/diagnostic registration options.",
811
+ );
812
+ }
813
+ this.dynamicRegistrations.register(registration.method, registration.id);
814
+ }
815
+ return null;
816
+ }
817
+
818
+ /**
819
+ * Remove dynamic registrations for `textDocument/diagnostic`.
820
+ *
821
+ * The params key is the LSP specification's documented compatibility typo
822
+ * `unregisterations` (renamed to `unregistrations` only in a future 4.x).
823
+ * Capability loss disables pull as soon as the last id is removed.
824
+ */
825
+ private handleUnregisterCapability(params: unknown): null {
826
+ if (!isRecord(params) || !Array.isArray(params.unregisterations)) {
827
+ throw new JsonRpcRequestError(-32602, "Malformed client/unregisterCapability params.");
828
+ }
829
+ for (const entry of params.unregisterations) {
830
+ if (!isRecord(entry) || typeof entry.id !== "string" || typeof entry.method !== "string") {
831
+ throw new JsonRpcRequestError(-32602, "Malformed client/unregisterCapability entry.");
832
+ }
833
+ if (entry.method !== DOCUMENT_DIAGNOSTIC_METHOD) continue;
834
+ this.dynamicRegistrations.unregister(entry.method, entry.id);
835
+ }
836
+ return null;
837
+ }
838
+
491
839
  /** Apply a diagnostic publication received from the LSP transport. */
492
- handlePublishDiagnostics(params: PublishDiagnosticsParams): void {
840
+ handlePublishDiagnostics(params: unknown): void {
493
841
  this.diagnostics.handlePublishDiagnostics(params);
494
842
  }
495
843
 
@@ -499,9 +847,17 @@ export class LspClient {
499
847
  * Wait for the server to be ready to serve queries.
500
848
  * Returns immediately if already ready; returns the ongoing promise
501
849
  * if one is pending; creates and returns a new one otherwise.
850
+ * With request control, the caller's wait stops promptly on abort or
851
+ * deadline while the shared readiness state keeps its own lifecycle.
502
852
  */
503
- async getReady(): Promise<void> {
504
- if (this._isReady) return;
853
+ async getReady(control?: CodeRequestControl): Promise<void> {
854
+ const pending = this.pendingReady();
855
+ if (!control) return pending;
856
+ return raceRequestControl(pending, control);
857
+ }
858
+
859
+ private pendingReady(): Promise<void> {
860
+ if (this._isReady) return Promise.resolve();
505
861
  if (this._readyPromise !== null) return this._readyPromise;
506
862
  // If no progress timer was ever armed and no tokens are tracked,
507
863
  // the server was either never started with a real process (test scenario)
@@ -509,8 +865,11 @@ export class LspClient {
509
865
  // only when the client is still running — a crash or shutdown clears
510
866
  // both fields but must not report the client as ready.
511
867
  if (this.noProgressTimer === null && this.trackedTokens.size === 0) {
512
- if (this._status === "running") this._isReady = true;
513
- return;
868
+ if (this._status === "running") {
869
+ this._isReady = true;
870
+ this.everReady = true;
871
+ }
872
+ return Promise.resolve();
514
873
  }
515
874
  this._readyPromise = new Promise<void>((resolve, reject) => {
516
875
  this._readyResolve = resolve;
@@ -533,14 +892,20 @@ export class LspClient {
533
892
  source: "lsp",
534
893
  level: "debug",
535
894
  category: "readiness.progress-begin",
536
- message: `Readiness progress begin for token ${token}`,
537
- data: { token },
895
+ message: `Readiness progress begin for ${this.name}`,
896
+ cwd: boundCwd(this.cwd),
897
+ data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
538
898
  });
539
- // Cancel the 2s no-progress grace timer a server that sends
540
- // begin without a prior create is spec-deviant but valid.
899
+ // begin is the only transition that proves active work: it cancels
900
+ // the no-progress grace timer, blocks readiness, and arms the
901
+ // bounded per-token timeout. A server that sends begin without a
902
+ // prior create is spec-deviant but valid.
541
903
  this.cancelNoProgressTimer();
542
- this.trackedTokens.set(token, "begin-seen");
904
+ this.trackedTokens.set(token, "active");
905
+ this.tokenCreatedAt.delete(token);
906
+ const wasReady = this._isReady;
543
907
  this._isReady = false;
908
+ if (wasReady) this.publishLifecycle("readiness");
544
909
  // Re-arm readiness promise if not already pending
545
910
  if (!this._readyPromise) {
546
911
  this._readyPromise = new Promise<void>((resolve, reject) => {
@@ -557,22 +922,36 @@ export class LspClient {
557
922
  source: "lsp",
558
923
  level: "debug",
559
924
  category: "readiness.progress-end",
560
- message: `Readiness progress end for token ${token}`,
561
- data: { token },
925
+ message: `Readiness progress end for ${this.name}`,
926
+ cwd: boundCwd(this.cwd),
927
+ data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
562
928
  });
929
+ const state = this.trackedTokens.get(token);
930
+ if (state === undefined) return; // Unknown token: ignore fail-closed.
931
+ this.tokenCreatedAt.delete(token);
932
+ if (state === "created") {
933
+ // A pending token never blocked readiness; its end removes it.
934
+ this.trackedTokens.delete(token);
935
+ return;
936
+ }
563
937
  this.trackedTokens.set(token, "ended");
564
938
  this.clearTokenTimeout(token);
565
939
  this.checkAllTokensEnded();
566
940
  }
567
- // kind: "report" — intentionally no-op
941
+ // kind: "report" — intentionally no-op; active state is retained.
568
942
  }
569
943
 
570
- /** Check whether all tracked tokens have ended and resolve readiness. */
571
- private checkAllTokensEnded(): void {
572
- if (this.trackedTokens.size === 0) return;
944
+ /** Test whether any token has active (begun) work. */
945
+ private hasActiveTokens(): boolean {
573
946
  for (const state of this.trackedTokens.values()) {
574
- if (state !== "ended") return;
947
+ if (state === "active") return true;
575
948
  }
949
+ return false;
950
+ }
951
+
952
+ /** Resolve readiness when no token is active; pending tokens do not block. */
953
+ private checkAllTokensEnded(): void {
954
+ if (this.hasActiveTokens()) return;
576
955
  this.trackedTokens.clear();
577
956
  this.resolveReady();
578
957
  }
@@ -585,15 +964,35 @@ export class LspClient {
585
964
  this._readyResolve = undefined;
586
965
  this._readyReject = undefined;
587
966
  }
967
+ // A rejected or disposed client must never be marked ready again,
968
+ // even if a stray progress end arrives after the rejection.
969
+ if (this._status !== "running") return;
970
+ const becameReady = !this._isReady;
588
971
  this._isReady = true;
972
+ if (becameReady) this.everReady = true;
973
+ if (becameReady) this.publishLifecycle("readiness");
589
974
  recordDebugEvent({
590
975
  source: "lsp",
591
976
  level: "info",
592
977
  category: "readiness.resolved",
593
- message: `LSP client ${this.name} is ready (cwd: ${this.root})`,
978
+ message: `LSP client ${this.name} is ready`,
979
+ cwd: boundCwd(this.cwd),
980
+ data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
594
981
  });
595
982
  }
596
983
 
984
+ /**
985
+ * Tear down readiness state without a protocol shutdown.
986
+ *
987
+ * Clears pending and active progress tokens and rejects any pending
988
+ * readiness. Called when a client is discarded or its observer is
989
+ * disposed so token state cannot outlive the client.
990
+ */
991
+ dispose(): void {
992
+ this.cancelNoProgressTimer();
993
+ this.dynamicRegistrations.clear();
994
+ this.rejectReady(new Error("Client disposed"));
995
+ }
597
996
  /**
598
997
  * Reject the current readiness promise (if any) and mark the client
599
998
  * not ready. Called on shutdown, crash, or restart.
@@ -607,6 +1006,7 @@ export class LspClient {
607
1006
  }
608
1007
  this._isReady = false;
609
1008
  this.trackedTokens.clear();
1009
+ this.tokenCreatedAt.clear();
610
1010
  for (const timer of this.tokenTimeouts.values()) clearTimeout(timer);
611
1011
  this.tokenTimeouts.clear();
612
1012
  recordDebugEvent({
@@ -614,7 +1014,12 @@ export class LspClient {
614
1014
  level: this._status === "shutdown" ? "debug" : "warning",
615
1015
  category: "readiness.rejected",
616
1016
  message: `LSP client ${this.name} readiness rejected: ${reason.message}`,
617
- data: { status: this._status },
1017
+ cwd: boundCwd(this.cwd),
1018
+ data: {
1019
+ server: truncateIdentity(this.name),
1020
+ root: truncateIdentity(this.root),
1021
+ status: this._status,
1022
+ },
618
1023
  });
619
1024
  }
620
1025
 
@@ -634,8 +1039,9 @@ export class LspClient {
634
1039
  source: "lsp",
635
1040
  level: "debug",
636
1041
  category: "readiness.token-timeout",
637
- message: `Readiness per-token timeout fired for token ${token} after ${timeoutMs}ms`,
638
- data: { token, timeoutMs },
1042
+ message: `Readiness per-token timeout fired for ${this.name} after ${timeoutMs}ms`,
1043
+ cwd: boundCwd(this.cwd),
1044
+ data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root), timeoutMs },
639
1045
  });
640
1046
  this.checkAllTokensEnded();
641
1047
  }, timeoutMs);
@@ -661,6 +1067,8 @@ export class LspClient {
661
1067
  level: "debug",
662
1068
  category: "readiness.no-progress-cancelled",
663
1069
  message: `No-progress grace timer cancelled for ${this.name}`,
1070
+ cwd: boundCwd(this.cwd),
1071
+ data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
664
1072
  });
665
1073
  }
666
1074
  }
@@ -676,12 +1084,14 @@ export class LspClient {
676
1084
  */
677
1085
  private armNoProgressTimer(): void {
678
1086
  this.noProgressTimer = setTimeout(() => {
679
- if (this.trackedTokens.size === 0 && this._status === "running") {
1087
+ if (!this.hasActiveTokens() && this._status === "running") {
680
1088
  recordDebugEvent({
681
1089
  source: "lsp",
682
1090
  level: "debug",
683
1091
  category: "readiness.no-progress-resolved",
684
1092
  message: `No-progress grace timer resolved for ${this.name}`,
1093
+ cwd: boundCwd(this.cwd),
1094
+ data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
685
1095
  });
686
1096
  this.resolveReady();
687
1097
  }