@mrclrchtr/supi-code-intelligence 4.10.0 → 5.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 (203) hide show
  1. package/README.md +2 -1
  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 +1 -1
  10. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  13. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/README.md +80 -5
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
  28. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +206 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +114 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +372 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +14 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +491 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +71 -11
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +365 -297
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +52 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +185 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +467 -115
  44. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +161 -33
  45. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +10 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
  47. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +38 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
  50. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +13 -4
  51. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +300 -24
  52. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +464 -96
  54. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
  56. package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
  57. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
  59. package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
  60. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
  61. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +40 -0
  62. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +50 -0
  63. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +136 -120
  64. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +17 -7
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +6 -3
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
  122. package/package.json +5 -5
  123. package/src/analysis/health/diagnostics.ts +236 -24
  124. package/src/analysis/health/recovery.ts +6 -2
  125. package/src/analysis/provider.ts +58 -0
  126. package/src/analysis/readiness.ts +30 -9
  127. package/src/analysis/refactor/apply.ts +45 -1
  128. package/src/analysis/refactor/mutation-authority.ts +143 -0
  129. package/src/analysis/relations/callees.ts +9 -1
  130. package/src/analysis/relations/types.ts +5 -2
  131. package/src/analysis/search/ast-scan-timing.ts +9 -2
  132. package/src/analysis/search/ast-scan.ts +1 -1
  133. package/src/analysis/search/deadline.ts +2 -2
  134. package/src/analysis/search/pattern-analysis.ts +46 -32
  135. package/src/analysis/search/pattern.ts +12 -2
  136. package/src/analysis/target/anchored.ts +20 -11
  137. package/src/analysis/target/file.ts +18 -11
  138. package/src/analysis/target/identity.ts +19 -3
  139. package/src/analysis/target/symbol.ts +36 -15
  140. package/src/api.ts +4 -0
  141. package/src/diagnostics/evidence.ts +51 -0
  142. package/src/session/capability-adapter.ts +12 -3
  143. package/src/session/find-workflow.ts +17 -7
  144. package/src/session/graph/collect.ts +18 -2
  145. package/src/session/graph-workflow.ts +19 -12
  146. package/src/session/health-refresh.ts +148 -0
  147. package/src/session/health-types.ts +37 -15
  148. package/src/session/health-workflow.ts +37 -39
  149. package/src/session/inspect/collect.ts +20 -3
  150. package/src/session/inspect-workflow.ts +10 -5
  151. package/src/session/orientation/collect.ts +10 -1
  152. package/src/session/orientation/context-facts.ts +3 -1
  153. package/src/session/orientation/context-sections.ts +20 -7
  154. package/src/session/orientation/gather.ts +17 -11
  155. package/src/session/orientation-types.ts +2 -1
  156. package/src/session/orientation-workflow.ts +15 -8
  157. package/src/session/refactor-plans.ts +2 -0
  158. package/src/session/refactor-workflow.ts +59 -14
  159. package/src/session/session.ts +4 -1
  160. package/src/session/target-workflow.ts +54 -23
  161. package/src/session/workflow-control.ts +7 -3
  162. package/src/substrate/lsp/lifecycle.ts +26 -46
  163. package/src/substrate/lsp/maintenance.ts +199 -25
  164. package/src/substrate/lsp/state.ts +2 -2
  165. package/src/substrate/workspace-provider-host.ts +25 -3
  166. package/src/tool/find/execute.ts +7 -1
  167. package/src/tool/find/render.ts +20 -3
  168. package/src/tool/find/tui.ts +23 -21
  169. package/src/tool/graph/execute.ts +23 -3
  170. package/src/tool/graph/tui.ts +59 -46
  171. package/src/tool/health/execute.ts +7 -2
  172. package/src/tool/health/markdown.ts +56 -14
  173. package/src/tool/health/refresh-status.ts +105 -0
  174. package/src/tool/health/tui.ts +95 -98
  175. package/src/tool/infra/workflow-control.ts +8 -0
  176. package/src/tool/inspect/execute.ts +7 -1
  177. package/src/tool/inspect/tui.ts +26 -18
  178. package/src/tool/orientation/execute.ts +21 -3
  179. package/src/tool/orientation/tui.ts +51 -35
  180. package/src/tool/refactor-apply/execute.ts +3 -0
  181. package/src/tool/refactor-apply/tui.ts +43 -25
  182. package/src/tool/refactor-plan/execute.ts +4 -0
  183. package/src/tool/refactor-plan/tui.ts +33 -20
  184. package/src/tool/register.ts +127 -22
  185. package/src/tool/resolve/execute.ts +18 -1
  186. package/src/tool/resolve/tui.ts +42 -24
  187. package/src/tool/result/display.ts +107 -0
  188. package/src/tool/result/errors.ts +20 -17
  189. package/src/tool/result/find.ts +52 -8
  190. package/src/tool/result/graph.ts +44 -1
  191. package/src/tool/result/health.ts +53 -1
  192. package/src/tool/result/inspect.ts +113 -1
  193. package/src/tool/result/orientation.ts +66 -14
  194. package/src/tool/result/refactor.ts +36 -4
  195. package/src/tool/result/resolve.ts +44 -7
  196. package/src/tool/result/types.ts +20 -0
  197. package/src/types/execution.ts +18 -5
  198. package/src/types/index.ts +8 -1
  199. package/src/ui/status-command.ts +5 -3
  200. package/src/ui/status-overlay.ts +4 -0
  201. package/src/ui/tui/common.ts +108 -10
  202. package/src/ui/tui/display.ts +48 -0
  203. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -351
@@ -1,4 +1,18 @@
1
+ import * as path from "node:path";
2
+ import {
3
+ type CodeRequestControl,
4
+ isCodeRequestInterruption,
5
+ throwIfCodeRequestInterrupted,
6
+ } from "@mrclrchtr/supi-code-runtime/api";
7
+ import type { RecoveryRestartReason } from "../client/client.ts";
8
+ import { getDiagnosticFileState } from "../client/client-file-state.ts";
1
9
  import type { Diagnostic, FileEvent } from "../config/types.ts";
10
+ import {
11
+ type DiagnosticEvidenceDocument,
12
+ type DiagnosticEvidenceSummary,
13
+ emptyDiagnosticEvidence,
14
+ summarizeDiagnosticEvidence,
15
+ } from "../diagnostics/evidence.ts";
2
16
  import {
3
17
  assessStaleDiagnostics,
4
18
  type StaleDiagnosticAssessment,
@@ -8,18 +22,51 @@ export interface WorkspaceRecoveryResult {
8
22
  /** Active clients targeted by the best-effort refresh, not confirmed successful refreshes. */
9
23
  attemptedClients: number;
10
24
  restartedClients: number;
25
+ /** Server names of the active clients targeted by this pass, for telemetry identity. */
26
+ attemptedServers: string[];
27
+ /** Server names of the clients restarted during this pass, for telemetry identity. */
28
+ restartedServers: string[];
29
+ /** Stall signal of the first restarted route, when this pass restarted a client. */
30
+ restartReason?: RecoveryRestartReason;
31
+ /** Final document-level evidence from the last refresh in this recovery pass. */
32
+ diagnosticEvidence: DiagnosticEvidenceSummary;
33
+ /** Failure from the first refresh, when no later pass replaced it. */
34
+ refreshFailureReason?: string;
35
+ /** Wall-clock duration of the whole recovery pass, for telemetry. */
36
+ elapsedMs: number;
11
37
  staleAssessment: StaleDiagnosticAssessment;
12
38
  }
13
39
 
40
+ /** One client route's diagnostic evidence capability for recovery targeting. */
41
+ export interface WorkspaceDiagnosticRoute {
42
+ key: string;
43
+ /** False for push-only clients, which cannot confirm freshness by pull. */
44
+ supportsPull: boolean;
45
+ /** Files tracked by the route whose evidence is unconfirmed. */
46
+ unconfirmedFiles: string[];
47
+ /** Protocol-stall signal observed on the route, or null when healthy. */
48
+ stallSignal: RecoveryRestartReason | null;
49
+ }
50
+
14
51
  export interface WorkspaceRecoveryHost {
15
52
  clearAllPullResultIds(): void;
16
53
  notifyWorkspaceFileChanges(changes: FileEvent[]): void;
17
- refreshOpenDiagnostics(options?: { maxWaitMs?: number; quietMs?: number }): Promise<void>;
54
+ refreshOpenDiagnostics(
55
+ options?: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl,
56
+ ): Promise<DiagnosticEvidenceSummary>;
18
57
  getOutstandingDiagnostics(
19
58
  maxSeverity?: number,
20
59
  ): Array<{ file: string; diagnostics: Diagnostic[] }>;
21
- getStatus(): { servers: Array<{ status: "running" | "error" | "unavailable" }> };
22
- restartClientsForFiles(filePaths: string[]): Promise<string[]>;
60
+ getRunningClientCount(): number;
61
+ getRunningClientNames(): string[];
62
+ isDiagnosticFile(filePath: string): boolean;
63
+ getClientDiagnosticRoutes(): WorkspaceDiagnosticRoute[];
64
+ getDiagnosticEvidence(): DiagnosticEvidenceSummary;
65
+ getCwd(): string;
66
+ restartClientsForFiles(
67
+ filePaths: string[],
68
+ options?: { pushOnly?: boolean; control?: CodeRequestControl },
69
+ ): Promise<Array<{ key: string; serverName: string; files: string[]; restarted: boolean }>>;
23
70
  }
24
71
 
25
72
  /** Clear cached pull IDs and forward watched-file changes to active clients. */
@@ -29,10 +76,16 @@ export function softRecoverWorkspaceDiagnostics(
29
76
  ): number {
30
77
  host.clearAllPullResultIds();
31
78
  if (changes.length > 0) host.notifyWorkspaceFileChanges(changes);
32
- return countRunningClients(host);
79
+ return host.getRunningClientCount();
33
80
  }
34
81
 
35
- /** Run a recovery pass, refreshing diagnostics and escalating if stale state remains. */
82
+ /**
83
+ * Run a recovery pass, refreshing diagnostics and escalating if stale state remains.
84
+ *
85
+ * The pass observes request cancellation between its phases and propagates
86
+ * the interruption as a rejection instead of swallowing it. A pass that
87
+ * starts already cancelled rejects before any client or evidence work.
88
+ */
36
89
  export async function recoverWorkspaceDiagnostics(
37
90
  host: WorkspaceRecoveryHost,
38
91
  options: {
@@ -40,45 +93,268 @@ export async function recoverWorkspaceDiagnostics(
40
93
  restartIfStillStale?: boolean;
41
94
  maxWaitMs?: number;
42
95
  quietMs?: number;
96
+ control?: CodeRequestControl;
43
97
  } = {},
44
98
  ): Promise<WorkspaceRecoveryResult> {
99
+ // Reject immediately when the request was already cancelled: no client or
100
+ // evidence work may start for a pass the caller no longer awaits.
101
+ throwIfCodeRequestInterrupted(options.control);
102
+ const recoveryStartedAt = Date.now();
45
103
  const attemptedClients = softRecoverWorkspaceDiagnostics(host, options.changes ?? []);
104
+ const attemptedServers = host.getRunningClientNames();
105
+ let diagnosticEvidence = emptyDiagnosticEvidence();
106
+ let refreshFailureReason: string | undefined;
46
107
 
47
108
  try {
48
- await host.refreshOpenDiagnostics({ maxWaitMs: options.maxWaitMs, quietMs: options.quietMs });
49
- } catch {
50
- // Recovery should be best-effort.
109
+ diagnosticEvidence = await host.refreshOpenDiagnostics({
110
+ maxWaitMs: options.maxWaitMs,
111
+ quietMs: options.quietMs,
112
+ ...options.control,
113
+ });
114
+ } catch (error) {
115
+ if (isCodeRequestInterruption(error, options.control)) throw error;
116
+ refreshFailureReason = errorMessage(error);
117
+ diagnosticEvidence = host.getDiagnosticEvidence();
51
118
  }
52
119
 
53
120
  let staleAssessment = assessStaleDiagnostics(host.getOutstandingDiagnostics(1));
54
121
  let restartedClients = 0;
122
+ let restartServerNames: string[] = [];
123
+ let restartReason: RecoveryRestartReason | undefined;
55
124
 
56
- if (options.restartIfStillStale && staleAssessment.suspected) {
57
- const restarted = await host.restartClientsForFiles(
58
- staleAssessment.matchedFiles.map((entry) => entry.file),
59
- );
125
+ if (options.restartIfStillStale) {
126
+ // Observe cancellation between pass phases: an abort that arrived during
127
+ // the first refresh stops the pass before any client restart.
128
+ throwIfCodeRequestInterrupted(options.control);
129
+ const restartTargets = collectRecoveryRestartTargets(host);
130
+ const restartFiles = restartTargets.flatMap((target) => target.files);
131
+ if (restartFiles.length > 0) {
132
+ const escalation = await runRestartEscalation(host, {
133
+ restartFiles,
134
+ restartTargets,
135
+ evidence: diagnosticEvidence,
136
+ ...options,
137
+ });
138
+ restartedClients = escalation.restartedClients;
139
+ restartServerNames = escalation.restartedServerNames;
140
+ restartReason = escalation.restartReason;
141
+ diagnosticEvidence = escalation.diagnosticEvidence;
142
+ if (escalation.refreshedAfterRestart) refreshFailureReason = undefined;
143
+ staleAssessment = assessStaleDiagnostics(host.getOutstandingDiagnostics(1));
144
+ }
145
+ }
146
+
147
+ // Observe cancellation before returning regardless of escalation: a
148
+ // refresh-only pass must not report a clean result after the caller
149
+ // cancelled the request.
150
+ throwIfCodeRequestInterrupted(options.control);
151
+
152
+ return recoveryResult();
153
+
154
+ function recoveryResult(): WorkspaceRecoveryResult {
155
+ return {
156
+ attemptedClients,
157
+ restartedClients,
158
+ attemptedServers,
159
+ restartedServers: restartServerNames,
160
+ ...(restartReason ? { restartReason } : {}),
161
+ diagnosticEvidence,
162
+ ...(refreshFailureReason ? { refreshFailureReason } : {}),
163
+ elapsedMs: Date.now() - recoveryStartedAt,
164
+ staleAssessment,
165
+ };
166
+ }
167
+ }
168
+
169
+ /** One push-only route that a recovery pass may restart. */
170
+ interface RecoveryRestartTarget {
171
+ key: string;
172
+ reason: RecoveryRestartReason;
173
+ files: string[];
174
+ }
175
+
176
+ /**
177
+ * Collect restart targets: push-only routes with a protocol-stall signal.
178
+ *
179
+ * Unconfirmed evidence alone never restarts a client: the reopen-resync
180
+ * fallback handles unconfirmed push-only documents without discarding
181
+ * server state (ADR 0020). Restarts require a readiness stall or repeated
182
+ * protocol failures, and stay limited to push-only routes.
183
+ */
184
+ function collectRecoveryRestartTargets(host: WorkspaceRecoveryHost): RecoveryRestartTarget[] {
185
+ const targets: RecoveryRestartTarget[] = [];
186
+ for (const route of host.getClientDiagnosticRoutes()) {
187
+ if (route.supportsPull) continue;
188
+ if (!route.stallSignal) continue;
189
+ if (route.unconfirmedFiles.length === 0) continue;
190
+ targets.push({
191
+ key: route.key,
192
+ reason: route.stallSignal,
193
+ files: route.unconfirmedFiles,
194
+ });
195
+ }
196
+ return targets;
197
+ }
198
+
199
+ /**
200
+ * Restart affected push-only clients and merge the replacement refresh evidence.
201
+ *
202
+ * Cancellation observed during the escalation is propagated as a rejection;
203
+ * only non-interruption failures degrade to unconfirmed evidence.
204
+ */
205
+ async function runRestartEscalation(
206
+ host: WorkspaceRecoveryHost,
207
+ options: {
208
+ restartFiles: readonly string[];
209
+ restartTargets: readonly RecoveryRestartTarget[];
210
+ evidence: DiagnosticEvidenceSummary;
211
+ maxWaitMs?: number;
212
+ quietMs?: number;
213
+ control?: CodeRequestControl;
214
+ },
215
+ ): Promise<{
216
+ restartedClients: number;
217
+ restartedServerNames: string[];
218
+ restartReason?: RecoveryRestartReason;
219
+ diagnosticEvidence: DiagnosticEvidenceSummary;
220
+ refreshedAfterRestart: boolean;
221
+ }> {
222
+ const { restartFiles, restartTargets, evidence } = options;
223
+ const reasonByKey = new Map(restartTargets.map((target) => [target.key, target.reason]));
224
+ let diagnosticEvidence = evidence;
225
+ let restartedClients = 0;
226
+ let restartedServerNames: string[] = [];
227
+ let restartReason: RecoveryRestartReason | undefined;
228
+ let refreshedAfterRestart = false;
229
+
230
+ try {
231
+ const restartResults = await host.restartClientsForFiles([...restartFiles], {
232
+ pushOnly: true,
233
+ control: options.control,
234
+ });
235
+ // Observe cancellation once the restart loop settles: a cancelled pass
236
+ // must not invalidate or refresh evidence for replacement processes.
237
+ throwIfCodeRequestInterrupted(options.control);
238
+ const restarted = restartResults.filter((result) => result.restarted);
60
239
  restartedClients = restarted.length;
240
+ restartedServerNames = restarted.map((result) => result.serverName);
241
+ restartReason = firstRestartReason(restarted, reasonByKey);
242
+ // Every attempted route had its client replaced or shut down; its owned
243
+ // files need fresh evidence. Routes the manager skipped (pull-capable or
244
+ // guard-blocked) produce no result and keep their refreshed evidence.
245
+ const attemptedFiles = restartResults
246
+ .flatMap((result) => result.files)
247
+ .filter((file) => host.isDiagnosticFile(file));
248
+ diagnosticEvidence = invalidateDiagnosticEvidence(evidence, attemptedFiles, host.getCwd());
61
249
 
62
- if (restartedClients > 0) {
250
+ if (restarted.length > 0) {
63
251
  try {
64
- await host.refreshOpenDiagnostics({
65
- maxWaitMs: options.maxWaitMs,
66
- quietMs: options.quietMs,
67
- });
68
- } catch {
69
- // Keep the previous assessment if the follow-up refresh fails.
252
+ diagnosticEvidence = mergeDiagnosticEvidence(
253
+ diagnosticEvidence,
254
+ await host.refreshOpenDiagnostics({
255
+ maxWaitMs: options.maxWaitMs,
256
+ quietMs: options.quietMs,
257
+ ...options.control,
258
+ }),
259
+ host.getCwd(),
260
+ );
261
+ // Observe cancellation after the replacement refresh: a cancelled
262
+ // pass discards the fresh result instead of reporting it as current.
263
+ throwIfCodeRequestInterrupted(options.control);
264
+ refreshedAfterRestart = true;
265
+ } catch (error) {
266
+ if (isCodeRequestInterruption(error, options.control)) throw error;
267
+ // Keep affected evidence unconfirmed when replacement refresh fails.
70
268
  }
71
- staleAssessment = assessStaleDiagnostics(host.getOutstandingDiagnostics(1));
72
269
  }
270
+ } catch (error) {
271
+ if (isCodeRequestInterruption(error, options.control)) throw error;
272
+ // Keep affected evidence unconfirmed when replacement fails.
73
273
  }
74
274
 
75
275
  return {
76
- attemptedClients,
77
276
  restartedClients,
78
- staleAssessment,
277
+ restartedServerNames,
278
+ ...(restartReason ? { restartReason } : {}),
279
+ diagnosticEvidence,
280
+ refreshedAfterRestart,
79
281
  };
80
282
  }
81
283
 
82
- function countRunningClients(host: WorkspaceRecoveryHost): number {
83
- return host.getStatus().servers.filter((server) => server.status === "running").length;
284
+ /** Return the stall signal of the first route that actually restarted. */
285
+ function firstRestartReason(
286
+ restarted: Array<{ key: string }>,
287
+ reasonByKey: ReadonlyMap<string, RecoveryRestartReason>,
288
+ ): RecoveryRestartReason | undefined {
289
+ for (const result of restarted) {
290
+ const reason = reasonByKey.get(result.key);
291
+ if (reason !== undefined) return reason;
292
+ }
293
+ return undefined;
294
+ }
295
+
296
+ function invalidateDiagnosticEvidence(
297
+ evidence: DiagnosticEvidenceSummary,
298
+ files: readonly string[],
299
+ cwd: string,
300
+ ): DiagnosticEvidenceSummary {
301
+ const affected = new Set(files.map((file) => path.resolve(cwd, file)));
302
+ const documents = evidence.documents.map((document) =>
303
+ affected.has(path.resolve(cwd, document.file))
304
+ ? {
305
+ ...document,
306
+ status: invalidatedFileStatus(path.resolve(cwd, document.file)),
307
+ }
308
+ : document,
309
+ );
310
+ const known = new Set(documents.map((document) => path.resolve(cwd, document.file)));
311
+ for (const file of files) {
312
+ const resolved = path.resolve(cwd, file);
313
+ if (!known.has(resolved)) {
314
+ documents.push({
315
+ file: path.relative(cwd, resolved),
316
+ status: invalidatedFileStatus(resolved),
317
+ });
318
+ known.add(resolved);
319
+ }
320
+ }
321
+ return summarizeDiagnosticEvidence(documents as DiagnosticEvidenceDocument[]);
322
+ }
323
+
324
+ function invalidatedFileStatus(filePath: string): "unconfirmed" | "failed" | "removed" {
325
+ switch (getDiagnosticFileState(filePath)) {
326
+ case "removed":
327
+ return "removed";
328
+ case "unreadable":
329
+ return "failed";
330
+ default:
331
+ return "unconfirmed";
332
+ }
333
+ }
334
+
335
+ function errorMessage(error: unknown): string {
336
+ return error instanceof Error && error.message ? error.message : "Diagnostic refresh failed.";
337
+ }
338
+
339
+ function mergeDiagnosticEvidence(
340
+ invalidated: DiagnosticEvidenceSummary,
341
+ refreshed: DiagnosticEvidenceSummary,
342
+ cwd: string,
343
+ ): DiagnosticEvidenceSummary {
344
+ const documents = new Map<string, DiagnosticEvidenceDocument>();
345
+ for (const document of invalidated.documents) {
346
+ const resolved = path.resolve(cwd, document.file);
347
+ documents.set(resolved, {
348
+ file: path.relative(cwd, resolved),
349
+ status: document.status,
350
+ });
351
+ }
352
+ for (const document of refreshed.documents) {
353
+ const resolved = path.resolve(cwd, document.file);
354
+ documents.set(resolved, {
355
+ file: path.relative(cwd, resolved),
356
+ status: document.status,
357
+ });
358
+ }
359
+ return summarizeDiagnosticEvidence(Array.from(documents.values()));
84
360
  }
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import {
4
4
  type CodeQueryResult,
5
+ type CodeRequestControl,
5
6
  completedCodeQuery,
6
7
  partialCodeQuery,
7
8
  unavailableCodeQuery,
@@ -55,6 +56,7 @@ export interface WorkspaceSymbolCollection {
55
56
  export async function collectWorkspaceSymbols(
56
57
  clients: Iterable<LspClient>,
57
58
  query: string,
59
+ control?: CodeRequestControl,
58
60
  ): Promise<WorkspaceSymbolCollection> {
59
61
  const results: WorkspaceSymbolLike[] = [];
60
62
  const failures: string[] = [];
@@ -65,7 +67,7 @@ export async function collectWorkspaceSymbols(
65
67
  if (client.status !== "running") continue;
66
68
  if (!client.serverCapabilities?.workspaceSymbolProvider) continue;
67
69
  hasSupport = true;
68
- const result = await client.workspaceSymbol(query);
70
+ const result = await client.workspaceSymbol(query, control);
69
71
  if (result.kind === "unavailable") {
70
72
  failures.push(result.reason);
71
73
  continue;
@@ -99,8 +101,9 @@ export function workspaceSymbolCollectionResult(
99
101
  export async function managerWorkspaceSymbol(
100
102
  clients: Iterable<LspClient>,
101
103
  query: string,
104
+ control?: CodeRequestControl,
102
105
  ): Promise<CodeQueryResult<WorkspaceSymbolLike[]>> {
103
- return workspaceSymbolCollectionResult(await collectWorkspaceSymbols(clients, query));
106
+ return workspaceSymbolCollectionResult(await collectWorkspaceSymbols(clients, query, control));
104
107
  }
105
108
 
106
109
  export function findWorkspaceSymbolWarmTargets(