@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.12
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.
- package/CHANGELOG.md +60 -0
- package/dist/cli.js +3464 -3306
- package/dist/types/cli/bench-cli.d.ts +0 -5
- package/dist/types/cli/dry-balance-cli.d.ts +0 -5
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
- package/dist/types/config/model-registry.d.ts +1 -23
- package/dist/types/config/model-resolver.d.ts +10 -15
- package/dist/types/config/models-config-schema.d.ts +0 -6
- package/dist/types/config/models-config.d.ts +0 -6
- package/dist/types/config/settings-schema.d.ts +31 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +15 -0
- package/dist/types/extensibility/skills.d.ts +29 -0
- package/dist/types/lsp/client.d.ts +9 -3
- package/dist/types/modes/components/todo-reminder.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +0 -1
- package/dist/types/modes/skill-command.d.ts +1 -1
- package/dist/types/modes/types.d.ts +0 -1
- package/dist/types/stt/asr-client.d.ts +7 -3
- package/dist/types/task/spawn-policy.d.ts +17 -0
- package/dist/types/task/spawn-policy.test.d.ts +1 -0
- package/dist/types/task/types.d.ts +8 -2
- package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
- package/dist/types/tools/browser/registry.d.ts +2 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
- package/dist/types/tools/eval.d.ts +3 -6
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/image-vision-fallback.d.ts +1 -1
- package/package.json +14 -13
- package/scripts/bundle-dist.ts +23 -4
- package/scripts/generate-docs-index.ts +116 -24
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/async/job-manager.ts +27 -3
- package/src/auto-thinking/classifier.ts +1 -1
- package/src/cli/bench-cli.ts +3 -12
- package/src/cli/dry-balance-cli.ts +1 -6
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/models-cli.ts +3 -81
- package/src/commands/models.ts +1 -2
- package/src/commit/model-selection.ts +4 -4
- package/src/config/inline-tool-descriptors-mode.ts +1 -2
- package/src/config/model-discovery.ts +150 -82
- package/src/config/model-registry.ts +11 -225
- package/src/config/model-resolver.ts +23 -135
- package/src/config/models-config-schema.ts +0 -22
- package/src/config/prompt-templates.ts +20 -0
- package/src/config/settings-schema.ts +35 -1
- package/src/config/settings.ts +60 -24
- package/src/eval/__tests__/agent-bridge.test.ts +17 -3
- package/src/eval/agent-bridge.ts +7 -9
- package/src/eval/completion-bridge.ts +1 -1
- package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
- package/src/eval/py/runner.py +169 -18
- package/src/extensibility/extensions/model-api.ts +1 -3
- package/src/extensibility/extensions/runner.ts +62 -5
- package/src/extensibility/skills.ts +77 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/lsp/client.ts +162 -45
- package/src/lsp/config.ts +17 -3
- package/src/lsp/index.ts +31 -21
- package/src/main.ts +0 -1
- package/src/mcp/transports/http.ts +19 -17
- package/src/mcp/transports/stdio.test.ts +51 -1
- package/src/mcp/transports/stdio.ts +19 -9
- package/src/memories/index.ts +0 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/modes/acp/acp-agent.ts +6 -9
- package/src/modes/components/model-selector.ts +32 -186
- package/src/modes/components/todo-reminder.ts +5 -1
- package/src/modes/controllers/event-controller.ts +1 -16
- package/src/modes/controllers/selector-controller.ts +57 -36
- package/src/modes/interactive-mode.ts +0 -6
- package/src/modes/rpc/rpc-mode.ts +5 -8
- package/src/modes/skill-command.ts +8 -20
- package/src/modes/types.ts +0 -1
- package/src/prompts/system/subagent-system-prompt.md +2 -2
- package/src/prompts/system/tool-call-loop-redirect.md +8 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/sdk.ts +2 -9
- package/src/session/agent-session.ts +89 -8
- package/src/session/session-context.ts +2 -1
- package/src/session/session-manager.ts +2 -1
- package/src/session/unexpected-stop-classifier.ts +1 -1
- package/src/stt/asr-client.ts +87 -27
- package/src/stt/downloader.ts +8 -2
- package/src/task/executor.ts +1 -3
- package/src/task/index.ts +37 -32
- package/src/task/spawn-policy.test.ts +63 -0
- package/src/task/spawn-policy.ts +58 -0
- package/src/task/types.ts +77 -6
- package/src/tools/__tests__/eval-description.test.ts +19 -0
- package/src/tools/ast-grep.ts +34 -12
- package/src/tools/browser/cmux/cmux-tab.ts +66 -24
- package/src/tools/browser/registry.ts +25 -0
- package/src/tools/browser/run-cancellation.ts +47 -0
- package/src/tools/browser/tab-supervisor.ts +120 -7
- package/src/tools/browser/tab-worker.ts +22 -10
- package/src/tools/browser.ts +1 -0
- package/src/tools/eval.ts +15 -10
- package/src/tools/grep.ts +11 -8
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/write.ts +49 -9
- package/src/utils/commit-message-generator.ts +0 -1
- package/src/utils/git.ts +22 -1
- package/src/utils/image-vision-fallback.ts +2 -3
- package/src/utils/title-generator.ts +1 -1
package/src/lsp/client.ts
CHANGED
|
@@ -180,22 +180,91 @@ const CLIENT_CAPABILITIES = {
|
|
|
180
180
|
// LSP Message Protocol
|
|
181
181
|
// =============================================================================
|
|
182
182
|
|
|
183
|
+
function abortReason(signal: AbortSignal): Error {
|
|
184
|
+
return signal.reason instanceof Error ? signal.reason : new ToolAbortError();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
class LspFlushAbortError extends Error {
|
|
188
|
+
constructor(readonly reason: Error) {
|
|
189
|
+
super(reason.message);
|
|
190
|
+
this.name = "LspFlushAbortError";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
183
194
|
async function writeMessage(
|
|
184
195
|
sink: Bun.FileSink,
|
|
185
196
|
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
197
|
+
signal?: AbortSignal,
|
|
186
198
|
): Promise<void> {
|
|
199
|
+
if (signal?.aborted) {
|
|
200
|
+
throw abortReason(signal);
|
|
201
|
+
}
|
|
187
202
|
const content = JSON.stringify(message);
|
|
188
203
|
sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n${content}`);
|
|
189
|
-
|
|
204
|
+
const flush = Promise.resolve(sink.flush());
|
|
205
|
+
if (!signal) {
|
|
206
|
+
await flush;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// The sink's flush blocks on the OS-level pipe drain: if the server is
|
|
210
|
+
// alive but stopped reading stdin, `await sink.flush()` never resolves.
|
|
211
|
+
// Race the flush against the caller's signal so a wedged server surfaces
|
|
212
|
+
// as the tool's normal timeout/cancel instead of a permanent hang.
|
|
213
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
214
|
+
const onAbort = () => {
|
|
215
|
+
signal.removeEventListener("abort", onAbort);
|
|
216
|
+
// The underlying flush stays pending in the background; suppress its
|
|
217
|
+
// eventual settlement so we do not surface an unhandled rejection.
|
|
218
|
+
flush.catch(() => {});
|
|
219
|
+
reject(new LspFlushAbortError(abortReason(signal)));
|
|
220
|
+
};
|
|
221
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
222
|
+
flush.then(
|
|
223
|
+
() => {
|
|
224
|
+
signal.removeEventListener("abort", onAbort);
|
|
225
|
+
resolve();
|
|
226
|
+
},
|
|
227
|
+
(err: unknown) => {
|
|
228
|
+
signal.removeEventListener("abort", onAbort);
|
|
229
|
+
reject(err);
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
await promise;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Kill a client whose write queue is stuck (aborted flush left the sink's
|
|
237
|
+
* flush promise pending, so subsequent writes queue behind a wedge forever).
|
|
238
|
+
* Remove it from `clients` immediately so concurrent `getOrCreateClient`
|
|
239
|
+
* callers do not grab the corpse before `proc.exited` cleans up.
|
|
240
|
+
*/
|
|
241
|
+
function teardownWedgedClient(client: LspClient): void {
|
|
242
|
+
if (clients.get(client.name) === client) clients.delete(client.name);
|
|
243
|
+
try {
|
|
244
|
+
client.proc.kill();
|
|
245
|
+
} catch {
|
|
246
|
+
// process already gone or unkillable — the exit handler will finish cleanup.
|
|
247
|
+
}
|
|
190
248
|
}
|
|
191
249
|
|
|
192
250
|
function queueWriteMessage(
|
|
193
251
|
client: LspClient,
|
|
194
252
|
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
253
|
+
signal?: AbortSignal,
|
|
195
254
|
): Promise<void> {
|
|
196
|
-
const write = client.writeQueue.catch(() => {}).then(() => writeMessage(client.proc.stdin, message));
|
|
197
|
-
|
|
198
|
-
|
|
255
|
+
const write = client.writeQueue.catch(() => {}).then(() => writeMessage(client.proc.stdin, message, signal));
|
|
256
|
+
const result = write.catch((err: unknown) => {
|
|
257
|
+
if (err instanceof LspFlushAbortError) {
|
|
258
|
+
// Only an abort that raced this write's in-flight flush leaves
|
|
259
|
+
// the sink pending. Pre-write aborts and queued caller timeouts
|
|
260
|
+
// must not kill a healthy shared client.
|
|
261
|
+
teardownWedgedClient(client);
|
|
262
|
+
throw err.reason;
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
265
|
+
});
|
|
266
|
+
client.writeQueue = result.catch(() => {});
|
|
267
|
+
return result;
|
|
199
268
|
}
|
|
200
269
|
|
|
201
270
|
// =============================================================================
|
|
@@ -530,9 +599,18 @@ const EXIT_TIMEOUT_MS = 1_000;
|
|
|
530
599
|
* Get or create an LSP client for the given server configuration and working directory.
|
|
531
600
|
* @param config - Server configuration
|
|
532
601
|
* @param cwd - Working directory
|
|
533
|
-
* @param initTimeoutMs - Optional
|
|
602
|
+
* @param initTimeoutMs - Optional hard deadline for the initialize handshake (warmup / other
|
|
603
|
+
* short-lived callers). When set it takes precedence over `signal` inside `sendRequest`.
|
|
604
|
+
* @param signal - Optional caller abort signal. Threaded into the initialize `sendRequest`
|
|
605
|
+
* and the `initialized` notification so a wedged server surfaces the caller's
|
|
606
|
+
* timeout/cancel instead of falling back to the internal 30s default.
|
|
534
607
|
*/
|
|
535
|
-
export async function getOrCreateClient(
|
|
608
|
+
export async function getOrCreateClient(
|
|
609
|
+
config: ServerConfig,
|
|
610
|
+
cwd: string,
|
|
611
|
+
initTimeoutMs?: number,
|
|
612
|
+
signal?: AbortSignal,
|
|
613
|
+
): Promise<LspClient> {
|
|
536
614
|
const key = `${config.command}:${cwd}`;
|
|
537
615
|
|
|
538
616
|
// Check if client already exists
|
|
@@ -649,7 +727,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
649
727
|
initializationOptions: config.initOptions ?? {},
|
|
650
728
|
workspaceFolders: currentWorkspaceFolders(client),
|
|
651
729
|
},
|
|
652
|
-
|
|
730
|
+
signal,
|
|
653
731
|
initTimeoutMs,
|
|
654
732
|
)) as { capabilities?: unknown };
|
|
655
733
|
|
|
@@ -660,7 +738,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
660
738
|
client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
|
|
661
739
|
|
|
662
740
|
// Send initialized notification
|
|
663
|
-
await sendNotification(client, "initialized", {});
|
|
741
|
+
await sendNotification(client, "initialized", {}, signal);
|
|
664
742
|
|
|
665
743
|
client.status = "ready";
|
|
666
744
|
// Publish only after init succeeds: pre-init clients are reachable
|
|
@@ -676,10 +754,10 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
676
754
|
proc.kill();
|
|
677
755
|
const message = err instanceof Error ? err.message : String(err);
|
|
678
756
|
// Negative-cache deterministic failures. Timeouts under a
|
|
679
|
-
// caller-shortened deadline (warmup/writethrough)
|
|
680
|
-
// the server may simply be slow
|
|
681
|
-
// deadline
|
|
682
|
-
if (!(initTimeoutMs !== undefined && message.includes("timed out"))) {
|
|
757
|
+
// caller-shortened deadline (warmup/writethrough) and caller-signal
|
|
758
|
+
// aborts are transient — the server may simply be slow or the user may
|
|
759
|
+
// have cancelled, so a later call with a fresh deadline should retry.
|
|
760
|
+
if (!signal?.aborted && !(initTimeoutMs !== undefined && message.includes("timed out"))) {
|
|
683
761
|
initFailures.set(key, { at: Date.now(), message });
|
|
684
762
|
}
|
|
685
763
|
throw err;
|
|
@@ -732,14 +810,19 @@ export async function ensureFileOpen(client: LspClient, filePath: string, signal
|
|
|
732
810
|
const languageId = detectLanguageId(filePath);
|
|
733
811
|
throwIfAborted(signal);
|
|
734
812
|
|
|
735
|
-
await sendNotification(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
813
|
+
await sendNotification(
|
|
814
|
+
client,
|
|
815
|
+
"textDocument/didOpen",
|
|
816
|
+
{
|
|
817
|
+
textDocument: {
|
|
818
|
+
uri,
|
|
819
|
+
languageId,
|
|
820
|
+
version: 1,
|
|
821
|
+
text: content,
|
|
822
|
+
},
|
|
741
823
|
},
|
|
742
|
-
|
|
824
|
+
signal,
|
|
825
|
+
);
|
|
743
826
|
|
|
744
827
|
client.openFiles.set(uri, { version: 1, languageId });
|
|
745
828
|
client.lastActivity = Date.now();
|
|
@@ -800,14 +883,19 @@ export async function syncContent(
|
|
|
800
883
|
// Open file with provided content instead of reading from disk
|
|
801
884
|
const languageId = detectLanguageId(filePath);
|
|
802
885
|
throwIfAborted(signal);
|
|
803
|
-
await sendNotification(
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
886
|
+
await sendNotification(
|
|
887
|
+
client,
|
|
888
|
+
"textDocument/didOpen",
|
|
889
|
+
{
|
|
890
|
+
textDocument: {
|
|
891
|
+
uri,
|
|
892
|
+
languageId,
|
|
893
|
+
version: 1,
|
|
894
|
+
text: content,
|
|
895
|
+
},
|
|
809
896
|
},
|
|
810
|
-
|
|
897
|
+
signal,
|
|
898
|
+
);
|
|
811
899
|
client.openFiles.set(uri, { version: 1, languageId });
|
|
812
900
|
client.lastActivity = Date.now();
|
|
813
901
|
return;
|
|
@@ -815,10 +903,15 @@ export async function syncContent(
|
|
|
815
903
|
|
|
816
904
|
const version = ++info.version;
|
|
817
905
|
throwIfAborted(signal);
|
|
818
|
-
await sendNotification(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
906
|
+
await sendNotification(
|
|
907
|
+
client,
|
|
908
|
+
"textDocument/didChange",
|
|
909
|
+
{
|
|
910
|
+
textDocument: { uri, version },
|
|
911
|
+
contentChanges: [{ text: content }],
|
|
912
|
+
},
|
|
913
|
+
signal,
|
|
914
|
+
);
|
|
822
915
|
client.lastActivity = Date.now();
|
|
823
916
|
})();
|
|
824
917
|
|
|
@@ -840,9 +933,14 @@ export async function notifySaved(client: LspClient, filePath: string, signal?:
|
|
|
840
933
|
if (!info) return; // File not open, nothing to notify
|
|
841
934
|
|
|
842
935
|
throwIfAborted(signal);
|
|
843
|
-
await sendNotification(
|
|
844
|
-
|
|
845
|
-
|
|
936
|
+
await sendNotification(
|
|
937
|
+
client,
|
|
938
|
+
"textDocument/didSave",
|
|
939
|
+
{
|
|
940
|
+
textDocument: { uri },
|
|
941
|
+
},
|
|
942
|
+
signal,
|
|
943
|
+
);
|
|
846
944
|
client.lastActivity = Date.now();
|
|
847
945
|
}
|
|
848
946
|
|
|
@@ -884,16 +982,26 @@ export async function refreshFile(client: LspClient, filePath: string, signal?:
|
|
|
884
982
|
const version = ++info.version;
|
|
885
983
|
throwIfAborted(signal);
|
|
886
984
|
|
|
887
|
-
await sendNotification(
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
985
|
+
await sendNotification(
|
|
986
|
+
client,
|
|
987
|
+
"textDocument/didChange",
|
|
988
|
+
{
|
|
989
|
+
textDocument: { uri, version },
|
|
990
|
+
contentChanges: [{ text: content }],
|
|
991
|
+
},
|
|
992
|
+
signal,
|
|
993
|
+
);
|
|
891
994
|
throwIfAborted(signal);
|
|
892
995
|
|
|
893
|
-
await sendNotification(
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
996
|
+
await sendNotification(
|
|
997
|
+
client,
|
|
998
|
+
"textDocument/didSave",
|
|
999
|
+
{
|
|
1000
|
+
textDocument: { uri },
|
|
1001
|
+
text: content,
|
|
1002
|
+
},
|
|
1003
|
+
signal,
|
|
1004
|
+
);
|
|
897
1005
|
|
|
898
1006
|
client.lastActivity = Date.now();
|
|
899
1007
|
})();
|
|
@@ -1044,8 +1152,10 @@ export async function sendRequest(
|
|
|
1044
1152
|
method,
|
|
1045
1153
|
});
|
|
1046
1154
|
|
|
1047
|
-
// Write request
|
|
1048
|
-
|
|
1155
|
+
// Write request. `queueWriteMessage(..., signal)` bounds the sink flush
|
|
1156
|
+
// so a wedged server does not stall the write queue past the signal's
|
|
1157
|
+
// deadline; the write-queue teardown kills the client on abort.
|
|
1158
|
+
queueWriteMessage(client, request, signal).catch(err => {
|
|
1049
1159
|
if (timeout) clearTimeout(timeout);
|
|
1050
1160
|
client.pendingRequests.delete(id);
|
|
1051
1161
|
cleanup();
|
|
@@ -1056,8 +1166,15 @@ export async function sendRequest(
|
|
|
1056
1166
|
|
|
1057
1167
|
/**
|
|
1058
1168
|
* Send an LSP notification (no response expected).
|
|
1169
|
+
* `signal` bounds the underlying `sink.flush()` — without it a server that
|
|
1170
|
+
* stops draining stdin blocks every future write on the client's write queue.
|
|
1059
1171
|
*/
|
|
1060
|
-
export async function sendNotification(
|
|
1172
|
+
export async function sendNotification(
|
|
1173
|
+
client: LspClient,
|
|
1174
|
+
method: string,
|
|
1175
|
+
params: unknown,
|
|
1176
|
+
signal?: AbortSignal,
|
|
1177
|
+
): Promise<void> {
|
|
1061
1178
|
const notification: LspJsonRpcNotification = {
|
|
1062
1179
|
jsonrpc: "2.0",
|
|
1063
1180
|
method,
|
|
@@ -1065,7 +1182,7 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
1065
1182
|
};
|
|
1066
1183
|
|
|
1067
1184
|
client.lastActivity = Date.now();
|
|
1068
|
-
await queueWriteMessage(client, notification);
|
|
1185
|
+
await queueWriteMessage(client, notification, signal);
|
|
1069
1186
|
}
|
|
1070
1187
|
|
|
1071
1188
|
/**
|
package/src/lsp/config.ts
CHANGED
|
@@ -209,13 +209,27 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
|
|
|
209
209
|
* Local bin directories to check before $PATH, ordered by priority.
|
|
210
210
|
* Each entry maps a root marker to the bin directory to check.
|
|
211
211
|
*/
|
|
212
|
+
const PYTHON_ROOT_MARKERS = [
|
|
213
|
+
"pyproject.toml",
|
|
214
|
+
"requirements.txt",
|
|
215
|
+
"setup.py",
|
|
216
|
+
"setup.cfg",
|
|
217
|
+
"Pipfile",
|
|
218
|
+
"pyrightconfig.json",
|
|
219
|
+
"ruff.toml",
|
|
220
|
+
".ruff.toml",
|
|
221
|
+
];
|
|
222
|
+
|
|
212
223
|
const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
|
|
213
224
|
// Node.js - check node_modules/.bin/
|
|
214
225
|
{ markers: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"], binDir: "node_modules/.bin" },
|
|
215
226
|
// Python - check virtual environment bin directories
|
|
216
|
-
{ markers:
|
|
217
|
-
{ markers:
|
|
218
|
-
{ markers:
|
|
227
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: ".venv/bin" },
|
|
228
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: ".venv/Scripts" },
|
|
229
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: "venv/bin" },
|
|
230
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: "venv/Scripts" },
|
|
231
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: ".env/bin" },
|
|
232
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: ".env/Scripts" },
|
|
219
233
|
// Ruby - check vendor bundle and binstubs
|
|
220
234
|
{ markers: ["Gemfile", "Gemfile.lock"], binDir: "vendor/bundle/bin" },
|
|
221
235
|
{ markers: ["Gemfile", "Gemfile.lock"], binDir: "bin" },
|
package/src/lsp/index.ts
CHANGED
|
@@ -216,7 +216,7 @@ async function syncFileContent(
|
|
|
216
216
|
if (serverConfig.createClient) {
|
|
217
217
|
return;
|
|
218
218
|
}
|
|
219
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
219
|
+
const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
|
|
220
220
|
throwIfAborted(signal);
|
|
221
221
|
await syncContent(client, absolutePath, content, signal);
|
|
222
222
|
}),
|
|
@@ -244,7 +244,7 @@ async function notifyFileSaved(
|
|
|
244
244
|
if (serverConfig.createClient) {
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
247
|
+
const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
|
|
248
248
|
await notifySaved(client, absolutePath, signal);
|
|
249
249
|
}),
|
|
250
250
|
);
|
|
@@ -469,7 +469,7 @@ async function reloadServer(client: LspClient, serverName: string, signal?: Abor
|
|
|
469
469
|
// as a request hangs until the tool deadline on servers that route it to
|
|
470
470
|
// the notification handler and never respond.
|
|
471
471
|
try {
|
|
472
|
-
await sendNotification(client, "workspace/didChangeConfiguration", { settings: {} });
|
|
472
|
+
await sendNotification(client, "workspace/didChangeConfiguration", { settings: {} }, signal);
|
|
473
473
|
return `Reloaded ${serverName}`;
|
|
474
474
|
} catch {
|
|
475
475
|
client.proc.kill();
|
|
@@ -643,12 +643,13 @@ async function captureDiagnosticVersions(
|
|
|
643
643
|
cwd: string,
|
|
644
644
|
servers: Array<[string, ServerConfig]>,
|
|
645
645
|
initTimeoutMs?: number,
|
|
646
|
+
signal?: AbortSignal,
|
|
646
647
|
): Promise<ServerVersionMap> {
|
|
647
648
|
const versions = new Map<string, number>();
|
|
648
649
|
await Promise.allSettled(
|
|
649
650
|
servers.map(async ([serverName, serverConfig]) => {
|
|
650
651
|
if (serverConfig.createClient) return;
|
|
651
|
-
const client = await getOrCreateClient(serverConfig, cwd, initTimeoutMs);
|
|
652
|
+
const client = await getOrCreateClient(serverConfig, cwd, initTimeoutMs, signal);
|
|
652
653
|
versions.set(serverName, client.diagnosticsVersion);
|
|
653
654
|
}),
|
|
654
655
|
);
|
|
@@ -659,12 +660,13 @@ async function captureOpenFileVersions(
|
|
|
659
660
|
absolutePath: string,
|
|
660
661
|
cwd: string,
|
|
661
662
|
servers: Array<[string, ServerConfig]>,
|
|
663
|
+
signal?: AbortSignal,
|
|
662
664
|
): Promise<ServerVersionMap> {
|
|
663
665
|
const uri = fileToUri(absolutePath);
|
|
664
666
|
const versions = new Map<string, number>();
|
|
665
667
|
await Promise.allSettled(
|
|
666
668
|
servers.map(async ([serverName, serverConfig]) => {
|
|
667
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
669
|
+
const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
|
|
668
670
|
const version = client.openFiles.get(uri)?.version;
|
|
669
671
|
if (version !== undefined) {
|
|
670
672
|
versions.set(serverName, version);
|
|
@@ -711,7 +713,7 @@ async function getDiagnosticsForFile(
|
|
|
711
713
|
}
|
|
712
714
|
|
|
713
715
|
// Default: use LSP
|
|
714
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
716
|
+
const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
|
|
715
717
|
throwIfAborted(signal);
|
|
716
718
|
if (isProjectAwareLspServer(serverConfig)) {
|
|
717
719
|
await waitForProjectLoaded(client, signal);
|
|
@@ -812,7 +814,7 @@ async function formatContent(
|
|
|
812
814
|
}
|
|
813
815
|
|
|
814
816
|
// Default: use LSP
|
|
815
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
817
|
+
const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
|
|
816
818
|
throwIfAborted(signal);
|
|
817
819
|
|
|
818
820
|
const caps = client.serverCapabilities;
|
|
@@ -1118,7 +1120,7 @@ async function runLspWritethrough(
|
|
|
1118
1120
|
// Capture diagnostic versions BEFORE syncing to detect stale diagnostics
|
|
1119
1121
|
// Bound client creation by the writethrough budget: a hung/broken server
|
|
1120
1122
|
// must not add its full init wait (30s default) to every edit.
|
|
1121
|
-
const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers, 5_000) : undefined;
|
|
1123
|
+
const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers, 5_000, signal) : undefined;
|
|
1122
1124
|
let expectedDocumentVersions: ServerVersionMap | undefined;
|
|
1123
1125
|
|
|
1124
1126
|
let formatter: FileFormatResult | undefined;
|
|
@@ -1163,7 +1165,7 @@ async function runLspWritethrough(
|
|
|
1163
1165
|
}
|
|
1164
1166
|
|
|
1165
1167
|
if (enableDiagnostics) {
|
|
1166
|
-
expectedDocumentVersions = await captureOpenFileVersions(dst, cwd, lspServers);
|
|
1168
|
+
expectedDocumentVersions = await captureOpenFileVersions(dst, cwd, lspServers, operationSignal);
|
|
1167
1169
|
}
|
|
1168
1170
|
|
|
1169
1171
|
// 5. Notify saved to LSP servers
|
|
@@ -1460,7 +1462,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1460
1462
|
allDiagnostics.push(...diagnostics);
|
|
1461
1463
|
continue;
|
|
1462
1464
|
}
|
|
1463
|
-
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1465
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd, undefined, signal);
|
|
1464
1466
|
if (isProjectAwareLspServer(serverConfig)) {
|
|
1465
1467
|
await waitForProjectLoaded(client, signal);
|
|
1466
1468
|
throwIfAborted(signal);
|
|
@@ -1634,7 +1636,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1634
1636
|
for (const [serverName, serverConfig] of servers) {
|
|
1635
1637
|
throwIfAborted(signal);
|
|
1636
1638
|
try {
|
|
1637
|
-
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1639
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd, undefined, signal);
|
|
1638
1640
|
if (isProjectAwareLspServer(serverConfig)) {
|
|
1639
1641
|
await waitForProjectLoaded(client, signal);
|
|
1640
1642
|
}
|
|
@@ -1781,16 +1783,14 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1781
1783
|
|
|
1782
1784
|
for (const [serverName, serverConfig] of servers) {
|
|
1783
1785
|
try {
|
|
1784
|
-
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1786
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd, undefined, signal);
|
|
1785
1787
|
for (const { oldUri } of pairs) {
|
|
1786
1788
|
if (client.openFiles.has(oldUri)) {
|
|
1787
|
-
await sendNotification(client, "textDocument/didClose", {
|
|
1788
|
-
textDocument: { uri: oldUri },
|
|
1789
|
-
});
|
|
1789
|
+
await sendNotification(client, "textDocument/didClose", { textDocument: { uri: oldUri } }, signal);
|
|
1790
1790
|
client.openFiles.delete(oldUri);
|
|
1791
1791
|
}
|
|
1792
1792
|
}
|
|
1793
|
-
await sendNotification(client, "workspace/didRenameFiles", lspParams);
|
|
1793
|
+
await sendNotification(client, "workspace/didRenameFiles", lspParams, signal);
|
|
1794
1794
|
} catch (err) {
|
|
1795
1795
|
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1796
1796
|
throw err;
|
|
@@ -1844,7 +1844,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1844
1844
|
for (const [serverName, serverConfig] of serverList) {
|
|
1845
1845
|
throwIfAborted(signal);
|
|
1846
1846
|
try {
|
|
1847
|
-
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1847
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd, undefined, signal);
|
|
1848
1848
|
respondingServers.add(serverName);
|
|
1849
1849
|
const caps = client.serverCapabilities ?? {};
|
|
1850
1850
|
sections.push(`${serverName}:`);
|
|
@@ -1930,7 +1930,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1930
1930
|
}
|
|
1931
1931
|
|
|
1932
1932
|
try {
|
|
1933
|
-
const client = await getOrCreateClient(chosenConfig, this.session.cwd);
|
|
1933
|
+
const client = await getOrCreateClient(chosenConfig, this.session.cwd, undefined, signal);
|
|
1934
1934
|
if (resolvedTarget) {
|
|
1935
1935
|
await ensureFileOpen(client, resolvedTarget, signal);
|
|
1936
1936
|
}
|
|
@@ -2001,7 +2001,12 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
2001
2001
|
for (const [workspaceServerName, workspaceServerConfig] of servers) {
|
|
2002
2002
|
throwIfAborted(signal);
|
|
2003
2003
|
try {
|
|
2004
|
-
const workspaceClient = await getOrCreateClient(
|
|
2004
|
+
const workspaceClient = await getOrCreateClient(
|
|
2005
|
+
workspaceServerConfig,
|
|
2006
|
+
this.session.cwd,
|
|
2007
|
+
undefined,
|
|
2008
|
+
signal,
|
|
2009
|
+
);
|
|
2005
2010
|
const workspaceResult = (await sendRequest(
|
|
2006
2011
|
workspaceClient,
|
|
2007
2012
|
"workspace/symbol",
|
|
@@ -2072,7 +2077,12 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
2072
2077
|
for (const [workspaceServerName, workspaceServerConfig] of servers) {
|
|
2073
2078
|
throwIfAborted(signal);
|
|
2074
2079
|
try {
|
|
2075
|
-
const workspaceClient = await getOrCreateClient(
|
|
2080
|
+
const workspaceClient = await getOrCreateClient(
|
|
2081
|
+
workspaceServerConfig,
|
|
2082
|
+
this.session.cwd,
|
|
2083
|
+
undefined,
|
|
2084
|
+
signal,
|
|
2085
|
+
);
|
|
2076
2086
|
outputs.push(await reloadServer(workspaceClient, workspaceServerName, signal));
|
|
2077
2087
|
} catch (err) {
|
|
2078
2088
|
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
@@ -2099,7 +2109,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
2099
2109
|
const [serverName, serverConfig] = serverInfo;
|
|
2100
2110
|
|
|
2101
2111
|
try {
|
|
2102
|
-
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
2112
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd, undefined, signal);
|
|
2103
2113
|
const targetFile = resolvedFile;
|
|
2104
2114
|
const isRustAnalyzerServer =
|
|
2105
2115
|
serverName === "rust-analyzer" ||
|
package/src/main.ts
CHANGED
|
@@ -238,8 +238,6 @@ export class HttpTransport implements MCPTransport {
|
|
|
238
238
|
signal: operation.signal,
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
-
operation.clear();
|
|
242
|
-
|
|
243
241
|
// Check for session ID in response
|
|
244
242
|
const newSessionId = response.headers.get("Mcp-Session-Id");
|
|
245
243
|
if (newSessionId) {
|
|
@@ -276,11 +274,12 @@ export class HttpTransport implements MCPTransport {
|
|
|
276
274
|
|
|
277
275
|
return result.result as T;
|
|
278
276
|
} catch (error) {
|
|
279
|
-
operation.clear();
|
|
280
277
|
if (operation.isTimeoutAbort(error)) {
|
|
281
278
|
throw new Error(`Request timeout after ${timeout}ms`);
|
|
282
279
|
}
|
|
283
280
|
throw error;
|
|
281
|
+
} finally {
|
|
282
|
+
operation.clear();
|
|
284
283
|
}
|
|
285
284
|
}
|
|
286
285
|
|
|
@@ -373,7 +372,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
373
372
|
headers["Mcp-Session-Id"] = this.#sessionId;
|
|
374
373
|
}
|
|
375
374
|
const timeout = resolveMCPTimeoutMs(this.config.timeout);
|
|
376
|
-
|
|
375
|
+
const operation = createMCPTimeout(timeout);
|
|
377
376
|
try {
|
|
378
377
|
const resp = await fetch(this.config.url, {
|
|
379
378
|
method: "POST",
|
|
@@ -381,7 +380,6 @@ export class HttpTransport implements MCPTransport {
|
|
|
381
380
|
body: JSON.stringify(body),
|
|
382
381
|
signal: operation.signal,
|
|
383
382
|
});
|
|
384
|
-
operation.clear();
|
|
385
383
|
// Retry once on auth failure if onAuthError is wired
|
|
386
384
|
if (this.onAuthError && (resp.status === 401 || resp.status === 403)) {
|
|
387
385
|
await resp.body?.cancel();
|
|
@@ -390,22 +388,27 @@ export class HttpTransport implements MCPTransport {
|
|
|
390
388
|
this.config.headers ??= {};
|
|
391
389
|
Object.assign(this.config.headers, newHeaders);
|
|
392
390
|
Object.assign(headers, newHeaders);
|
|
393
|
-
operation = createMCPTimeout(timeout);
|
|
394
|
-
const retry = await fetch(this.config.url, {
|
|
395
|
-
method: "POST",
|
|
396
|
-
headers,
|
|
397
|
-
body: JSON.stringify(body),
|
|
398
|
-
signal: operation.signal,
|
|
399
|
-
});
|
|
400
391
|
operation.clear();
|
|
401
|
-
|
|
392
|
+
const retryOperation = createMCPTimeout(timeout);
|
|
393
|
+
try {
|
|
394
|
+
const retry = await fetch(this.config.url, {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers,
|
|
397
|
+
body: JSON.stringify(body),
|
|
398
|
+
signal: retryOperation.signal,
|
|
399
|
+
});
|
|
400
|
+
await retry.body?.cancel();
|
|
401
|
+
} finally {
|
|
402
|
+
retryOperation.clear();
|
|
403
|
+
}
|
|
402
404
|
return;
|
|
403
405
|
}
|
|
404
406
|
}
|
|
405
407
|
await resp.body?.cancel();
|
|
406
408
|
} catch {
|
|
407
|
-
operation.clear();
|
|
408
409
|
// Best-effort response delivery — server may have disconnected
|
|
410
|
+
} finally {
|
|
411
|
+
operation.clear();
|
|
409
412
|
}
|
|
410
413
|
}
|
|
411
414
|
|
|
@@ -441,8 +444,6 @@ export class HttpTransport implements MCPTransport {
|
|
|
441
444
|
signal: operation.signal,
|
|
442
445
|
});
|
|
443
446
|
|
|
444
|
-
operation.clear();
|
|
445
|
-
|
|
446
447
|
// 202 Accepted is success for notifications
|
|
447
448
|
if (!response.ok && response.status !== 202) {
|
|
448
449
|
const text = await response.text();
|
|
@@ -465,11 +466,12 @@ export class HttpTransport implements MCPTransport {
|
|
|
465
466
|
await response.body?.cancel();
|
|
466
467
|
}
|
|
467
468
|
} catch (error) {
|
|
468
|
-
operation.clear();
|
|
469
469
|
if (operation.isTimeoutAbort(error)) {
|
|
470
470
|
throw new Error(`Notify timeout after ${timeout}ms`);
|
|
471
471
|
}
|
|
472
472
|
throw error;
|
|
473
|
+
} finally {
|
|
474
|
+
operation.clear();
|
|
473
475
|
}
|
|
474
476
|
}
|
|
475
477
|
|