diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 18:36:08 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 18:36:08 +0900 |
| commit | de022cee7ac66c95d7ed6a35d4e00f8e2d92cbbc (patch) | |
| tree | 041dcb1017e544a405526443cb578baa974bec0e /packages/lsp/src/extension.ts | |
| parent | fc1c3a54c3075990ec0dd0f97901bd46fe142923 (diff) | |
| parent | 649fc4f66f40f7743683546f81d3320e7394e597 (diff) | |
| download | dispatch-de022cee7ac66c95d7ed6a35d4e00f8e2d92cbbc.tar.gz dispatch-de022cee7ac66c95d7ed6a35d4e00f8e2d92cbbc.zip | |
Merge branch 'dev' into feature/ssh-support
Brings dev's retry-with-backoff (the transient `provider-retry` AgentEvent the
web frontend consumes) + the LSP-dead-server per-edit-hang fix into the SSH
feature branch, alongside the SSH waves 0-5c.
All code files auto-merged cleanly (run-turn.ts, orchestrator.ts, runtime.ts,
wire/index.ts, tool-edit-file/extension.ts, run-turn.test.ts — both computerId
threading and retry-with-backoff coexist). Only tasks.md conflicted (status
section — orchestrator-resolved; both feature sections kept).
Verified post-merge: tsc -b EXIT 0, biome clean (391 files), 1730 vitest pass
+6 sshd-integration skipped (was 1690; +40 from dev's retry/LSP tests).
Wire dist rebuilt so the FE can re-sync the pinned @dispatch/wire dep and pick
up BOTH provider-retry AND the SSH Computer/defaultComputerId types.
No merge or push (into dev or otherwise).
Diffstat (limited to 'packages/lsp/src/extension.ts')
| -rw-r--r-- | packages/lsp/src/extension.ts | 54 |
1 files changed, 25 insertions, 29 deletions
diff --git a/packages/lsp/src/extension.ts b/packages/lsp/src/extension.ts index 8e3178a..c0fee44 100644 --- a/packages/lsp/src/extension.ts +++ b/packages/lsp/src/extension.ts @@ -8,6 +8,7 @@ import { extname, join } from "node:path"; import type { Extension, HostAPI, ServiceHandle } from "@dispatch/kernel"; import { defineService } from "@dispatch/kernel"; +import { aggregateDiagnostics } from "./aggregate.js"; import type { SpawnedProcess } from "./client.js"; import { LspManager } from "./manager.js"; import { createLspTool } from "./tool.js"; @@ -43,6 +44,14 @@ function realSpawn( stderr: proc.stderr, pid: proc.pid, kill: () => proc.kill(), + // Surface process exit so the client can stop querying a dead server + // and self-heal (respawn). Bun's Subprocess.exited resolves with the + // exit code (or rejects if killed by signal — treat as code:null). + onExit: (handler) => { + (proc as { exited: Promise<number | null> }).exited + .then((code) => handler({ code })) + .catch(() => handler({ code: null })); + }, }; } @@ -108,14 +117,20 @@ export const extension: Extension = { return manager.status(cwd); }, async getDiagnostics(opts: GetDiagnosticsOpts): Promise<DiagnosticsResult> { - const timeoutMs = opts.timeoutMs ?? 60_000; - const slowThreshold = 10_000; + // 10s hard ceiling per server, regardless of what the caller + // passes (the edit hook still passes 60_000 — clamped here, so + // no other-unit edit is needed). A server that doesn't respond + // in 10s is skipped with a notice instead of waited out. + const PER_SERVER_CAP_MS = 10_000; + const timeoutMs = Math.min(opts.timeoutMs ?? PER_SERVER_CAP_MS, PER_SERVER_CAP_MS); const fileExt = extname(opts.filePath).toLowerCase(); const absolutePath = opts.filePath.startsWith("/") ? opts.filePath : join(opts.cwd, opts.filePath); // Get all connected servers matching this file's extension. + // A dead/corrupted server has state:"error" and is excluded — + // no per-edit hang on a corpse. const statuses = await manager.status(opts.cwd); const matching = statuses.filter( (s) => s.state === "connected" && s.extensions.some((ext) => ext === fileExt), @@ -125,34 +140,15 @@ export const extension: Extension = { return { formatted: "", slow: false, timedOut: false }; } - const parts: string[] = []; - let anySlow = false; - let anyTimedOut = false; - const start = Date.now(); - - for (const s of matching) { - const client = manager.getClient(s.id, s.root); - if (!client) continue; - const waitOpts: { text?: string; timeoutMs?: number; minSeverity?: number } = { - timeoutMs, - }; - if (opts.text !== undefined) waitOpts.text = opts.text; - if (opts.minSeverity !== undefined) waitOpts.minSeverity = opts.minSeverity; - const result = await client.waitForDiagnostics(absolutePath, waitOpts); - if (result.slow) anySlow = true; - if (result.timedOut) anyTimedOut = true; - if (result.formatted) { - parts.push(`[${s.name}]\n${result.formatted}`); - } - } - - const elapsed = Date.now() - start; + const agg = await aggregateDiagnostics( + (id, root) => manager.getClient(id, root), + matching, + absolutePath, + timeoutMs, + { text: opts.text, minSeverity: opts.minSeverity }, + ); - return { - formatted: parts.join("\n\n"), - slow: anySlow || elapsed > slowThreshold, - timedOut: anyTimedOut, - }; + return { formatted: agg.formatted, slow: false, timedOut: agg.timedOut }; }, }; host.provideService(lspServiceHandle, service); |
