@gajae-code/coding-agent 0.5.2 → 0.5.4
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 +23 -0
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +29 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/streaming-output.d.ts +12 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/web/search/providers/codex.d.ts +4 -4
- package/package.json +7 -7
- package/src/async/job-manager.ts +181 -43
- package/src/config/file-lock.ts +9 -1
- package/src/config/model-profile-activation.ts +71 -3
- package/src/config/model-profiles.ts +39 -14
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-runtime.ts +14 -13
- package/src/gjc-runtime/ralplan-runtime.ts +10 -0
- package/src/gjc-runtime/state-runtime.ts +73 -0
- package/src/gjc-runtime/tmux-gc.ts +86 -37
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/client.ts +64 -26
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/modes/bridge/bridge-mode.ts +21 -0
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/model-selector.ts +34 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +19 -0
- package/src/modes/controllers/selector-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +27 -0
- package/src/session/agent-session.ts +271 -25
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/session-manager.ts +29 -13
- package/src/session/streaming-output.ts +95 -3
- package/src/setup/model-onboarding-guidance.ts +10 -3
- package/src/skill-state/active-state.ts +79 -7
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/registry.ts +17 -1
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +2 -6
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/web/search/providers/codex.ts +6 -5
|
@@ -58,6 +58,19 @@ type TrackedPromise<T> = {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const STARTUP_TIMEOUT_MS = 250;
|
|
61
|
+
const STARTUP_TIMEOUT_GRACE_MS = 500;
|
|
62
|
+
const MAX_STARTUP_TIMEOUT_MS = 1_750;
|
|
63
|
+
|
|
64
|
+
function resolveStartupTimeoutMs(configs: MCPServerConfig[]): number {
|
|
65
|
+
const configuredTimeouts = configs
|
|
66
|
+
.map(config => config.timeout)
|
|
67
|
+
.filter((timeout): timeout is number => typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0);
|
|
68
|
+
if (configuredTimeouts.length === 0) return STARTUP_TIMEOUT_MS;
|
|
69
|
+
return Math.min(
|
|
70
|
+
MAX_STARTUP_TIMEOUT_MS,
|
|
71
|
+
Math.max(STARTUP_TIMEOUT_MS, Math.max(...configuredTimeouts) + STARTUP_TIMEOUT_GRACE_MS),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
61
74
|
|
|
62
75
|
function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
|
|
63
76
|
const tracked: TrackedPromise<T> = { promise, status: "pending" };
|
|
@@ -74,8 +87,20 @@ function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
|
|
|
74
87
|
return tracked;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
function delay(ms: number): Promise<void> {
|
|
78
|
-
return Bun.sleep(ms);
|
|
90
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
91
|
+
if (!signal) return Bun.sleep(ms);
|
|
92
|
+
if (signal.aborted) return Promise.reject(signal.reason ?? new Error("Aborted"));
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
signal.removeEventListener("abort", onAbort);
|
|
96
|
+
resolve();
|
|
97
|
+
}, ms);
|
|
98
|
+
const onAbort = () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
101
|
+
};
|
|
102
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
});
|
|
79
104
|
}
|
|
80
105
|
|
|
81
106
|
/**
|
|
@@ -166,11 +191,27 @@ export class MCPManager {
|
|
|
166
191
|
#pendingResourceRefresh = new Map<string, { connection: MCPServerConnection; promise: Promise<void> }>();
|
|
167
192
|
#pendingReconnections = new Map<string, Promise<MCPServerConnection | null>>();
|
|
168
193
|
#disconnectEpochs = new Map<string, number>();
|
|
194
|
+
#reconnectBackoffs = new Map<string, AbortController>();
|
|
169
195
|
/** Preserved configs for reconnection after connection loss. */
|
|
170
196
|
#serverConfigs = new Map<string, MCPServerConfig>();
|
|
171
197
|
/** Monotonic epoch incremented on disconnectAll to invalidate stale reconnections. */
|
|
172
198
|
#epoch = 0;
|
|
173
199
|
|
|
200
|
+
#isCurrentConnection(
|
|
201
|
+
name: string,
|
|
202
|
+
_config: MCPServerConfig,
|
|
203
|
+
globalEpoch: number,
|
|
204
|
+
disconnectEpoch: number,
|
|
205
|
+
connection: MCPServerConnection,
|
|
206
|
+
): boolean {
|
|
207
|
+
return (
|
|
208
|
+
this.#serverConfigs.has(name) &&
|
|
209
|
+
this.#epoch === globalEpoch &&
|
|
210
|
+
(this.#disconnectEpochs.get(name) ?? 0) === disconnectEpoch &&
|
|
211
|
+
this.#connections.get(name) === connection
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
174
215
|
constructor(
|
|
175
216
|
private cwd: string,
|
|
176
217
|
private toolCache: MCPToolCache | null = null,
|
|
@@ -305,6 +346,8 @@ export class MCPManager {
|
|
|
305
346
|
tracked: TrackedPromise<ToolLoadResult>;
|
|
306
347
|
toolsPromise: Promise<ToolLoadResult>;
|
|
307
348
|
connectionAbort: AbortController;
|
|
349
|
+
connectionEpoch: number;
|
|
350
|
+
disconnectEpoch: number;
|
|
308
351
|
};
|
|
309
352
|
|
|
310
353
|
const errors = new Map<string, string>();
|
|
@@ -312,6 +355,7 @@ export class MCPManager {
|
|
|
312
355
|
const allTools: CustomTool<TSchema, MCPToolDetails>[] = [];
|
|
313
356
|
const reportedErrors = new Set<string>();
|
|
314
357
|
let allowBackgroundLogging = false;
|
|
358
|
+
let shouldPublishToolSnapshot = true;
|
|
315
359
|
|
|
316
360
|
// Prepare connection tasks
|
|
317
361
|
const connectionTasks: ConnectionTask[] = [];
|
|
@@ -352,6 +396,7 @@ export class MCPManager {
|
|
|
352
396
|
this.#serverConfigs.set(name, config);
|
|
353
397
|
|
|
354
398
|
const connectionEpoch = this.#epoch;
|
|
399
|
+
const disconnectEpoch = this.#disconnectEpochs.get(name) ?? 0;
|
|
355
400
|
const connectionAbort = new AbortController();
|
|
356
401
|
this.#pendingConnectionControllers.set(name, connectionAbort);
|
|
357
402
|
// Resolve auth config before connecting, but do so per-server in parallel.
|
|
@@ -375,7 +420,10 @@ export class MCPManager {
|
|
|
375
420
|
connection._source = sources[name];
|
|
376
421
|
}
|
|
377
422
|
const stillPending = this.#pendingConnections.get(name) === connectionPromise;
|
|
378
|
-
const stillCurrent =
|
|
423
|
+
const stillCurrent =
|
|
424
|
+
this.#epoch === connectionEpoch &&
|
|
425
|
+
(this.#disconnectEpochs.get(name) ?? 0) === disconnectEpoch &&
|
|
426
|
+
this.#serverConfigs.get(name) === config;
|
|
379
427
|
if (stillPending) {
|
|
380
428
|
this.#pendingConnections.delete(name);
|
|
381
429
|
this.#pendingConnectionControllers.delete(name);
|
|
@@ -419,24 +467,45 @@ export class MCPManager {
|
|
|
419
467
|
this.#pendingConnections.set(name, connectionPromise);
|
|
420
468
|
|
|
421
469
|
const toolsPromise = connectionPromise.then(async connection => {
|
|
422
|
-
|
|
470
|
+
let serverTools: Awaited<ReturnType<typeof listTools>>;
|
|
471
|
+
try {
|
|
472
|
+
serverTools = await listTools(connection);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
connection.transport.onClose = undefined;
|
|
475
|
+
if (this.#connections.get(name) === connection) this.#connections.delete(name);
|
|
476
|
+
void connection.transport.close().catch(() => {});
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
if (!this.#isCurrentConnection(name, config, connectionEpoch, disconnectEpoch, connection)) {
|
|
480
|
+
connection.transport.onClose = undefined;
|
|
481
|
+
await connection.transport.close().catch(() => {});
|
|
482
|
+
throw new Error(`Server "${name}" was disconnected during tool loading`);
|
|
483
|
+
}
|
|
423
484
|
return { connection, serverTools };
|
|
424
485
|
});
|
|
425
486
|
this.#pendingToolLoads.set(name, toolsPromise);
|
|
426
487
|
|
|
427
488
|
const tracked = trackPromise(toolsPromise);
|
|
428
|
-
connectionTasks.push({
|
|
489
|
+
connectionTasks.push({
|
|
490
|
+
name,
|
|
491
|
+
config,
|
|
492
|
+
tracked,
|
|
493
|
+
toolsPromise,
|
|
494
|
+
connectionAbort,
|
|
495
|
+
connectionEpoch,
|
|
496
|
+
disconnectEpoch,
|
|
497
|
+
});
|
|
429
498
|
|
|
430
499
|
void toolsPromise
|
|
431
500
|
.then(async ({ connection, serverTools }) => {
|
|
432
501
|
if (this.#pendingToolLoads.get(name) !== toolsPromise) return;
|
|
502
|
+
if (!this.#isCurrentConnection(name, config, connectionEpoch, disconnectEpoch, connection)) return;
|
|
433
503
|
this.#pendingToolLoads.delete(name);
|
|
434
504
|
const reconnect = () => this.reconnectServer(name);
|
|
435
505
|
const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
|
|
436
506
|
this.#replaceServerTools(name, customTools);
|
|
437
507
|
this.#onToolsChanged?.(this.#tools);
|
|
438
508
|
void this.toolCache?.set(name, config, serverTools);
|
|
439
|
-
|
|
440
509
|
await this.#loadServerResourcesAndPrompts(name, connection);
|
|
441
510
|
})
|
|
442
511
|
.catch(error => {
|
|
@@ -454,9 +523,10 @@ export class MCPManager {
|
|
|
454
523
|
}
|
|
455
524
|
|
|
456
525
|
if (connectionTasks.length > 0) {
|
|
526
|
+
const startupTimeoutMs = resolveStartupTimeoutMs(connectionTasks.map(task => task.config));
|
|
457
527
|
await Promise.race([
|
|
458
528
|
Promise.allSettled(connectionTasks.map(task => task.tracked.promise)),
|
|
459
|
-
delay(
|
|
529
|
+
delay(startupTimeoutMs),
|
|
460
530
|
]);
|
|
461
531
|
|
|
462
532
|
const cachedTools = new Map<string, MCPToolDefinition[]>();
|
|
@@ -498,6 +568,13 @@ export class MCPManager {
|
|
|
498
568
|
const value = task.tracked.value;
|
|
499
569
|
if (!value) continue;
|
|
500
570
|
const { connection, serverTools } = value;
|
|
571
|
+
if (this.#pendingToolLoads.has(name) && this.#pendingToolLoads.get(name) !== task.toolsPromise) continue;
|
|
572
|
+
if (
|
|
573
|
+
!this.#isCurrentConnection(name, task.config, task.connectionEpoch, task.disconnectEpoch, connection)
|
|
574
|
+
) {
|
|
575
|
+
shouldPublishToolSnapshot = false;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
501
578
|
connectedServers.add(name);
|
|
502
579
|
const reconnect = () => this.reconnectServer(name);
|
|
503
580
|
allTools.push(...MCPTool.fromTools(connection, serverTools, reconnect));
|
|
@@ -506,6 +583,9 @@ export class MCPManager {
|
|
|
506
583
|
task.tracked.reason instanceof Error ? task.tracked.reason.message : String(task.tracked.reason);
|
|
507
584
|
errors.set(name, message);
|
|
508
585
|
reportedErrors.add(name);
|
|
586
|
+
if ((this.#disconnectEpochs.get(name) ?? 0) !== task.disconnectEpoch) {
|
|
587
|
+
shouldPublishToolSnapshot = false;
|
|
588
|
+
}
|
|
509
589
|
} else {
|
|
510
590
|
const cached = cachedTools.get(name);
|
|
511
591
|
if (cached) {
|
|
@@ -524,7 +604,7 @@ export class MCPManager {
|
|
|
524
604
|
sortMCPToolsByName(allTools);
|
|
525
605
|
|
|
526
606
|
// Update cached tools
|
|
527
|
-
this.#tools = allTools;
|
|
607
|
+
if (shouldPublishToolSnapshot) this.#tools = allTools;
|
|
528
608
|
allowBackgroundLogging = true;
|
|
529
609
|
|
|
530
610
|
return {
|
|
@@ -692,6 +772,8 @@ export class MCPManager {
|
|
|
692
772
|
this.#disconnectEpochs.set(name, nextEpoch);
|
|
693
773
|
this.#pendingConnectionControllers.get(name)?.abort(new Error(`MCP server disconnected: ${name}`));
|
|
694
774
|
this.#pendingConnectionControllers.delete(name);
|
|
775
|
+
this.#reconnectBackoffs.get(name)?.abort(new Error(`MCP server disconnected: ${name}`));
|
|
776
|
+
this.#reconnectBackoffs.delete(name);
|
|
695
777
|
this.#pendingConnections.delete(name);
|
|
696
778
|
this.#pendingToolLoads.delete(name);
|
|
697
779
|
this.#pendingReconnections.delete(name);
|
|
@@ -742,6 +824,10 @@ export class MCPManager {
|
|
|
742
824
|
this.#pendingConnectionControllers.clear();
|
|
743
825
|
this.#pendingConnections.clear();
|
|
744
826
|
this.#pendingToolLoads.clear();
|
|
827
|
+
for (const controller of this.#reconnectBackoffs.values()) {
|
|
828
|
+
controller.abort(new Error("MCP manager disconnected"));
|
|
829
|
+
}
|
|
830
|
+
this.#reconnectBackoffs.clear();
|
|
745
831
|
this.#pendingReconnections.clear();
|
|
746
832
|
this.#pendingResourceRefresh.clear();
|
|
747
833
|
this.#sources.clear();
|
|
@@ -764,7 +850,9 @@ export class MCPManager {
|
|
|
764
850
|
|
|
765
851
|
const attempt = this.#doReconnect(name);
|
|
766
852
|
this.#pendingReconnections.set(name, attempt);
|
|
767
|
-
return attempt.finally(() =>
|
|
853
|
+
return attempt.finally(() => {
|
|
854
|
+
if (this.#pendingReconnections.get(name) === attempt) this.#pendingReconnections.delete(name);
|
|
855
|
+
});
|
|
768
856
|
}
|
|
769
857
|
|
|
770
858
|
async #doReconnect(name: string): Promise<MCPServerConnection | null> {
|
|
@@ -777,60 +865,73 @@ export class MCPManager {
|
|
|
777
865
|
|
|
778
866
|
// Close the old transport without removing tools or notifying consumers.
|
|
779
867
|
// Tools stay available (stale) while we establish the new connection.
|
|
780
|
-
|
|
781
|
-
// DELETE with config.timeout (30s default), and blocking here delays the
|
|
782
|
-
// reconnect loop by that amount on every server restart.
|
|
783
|
-
const reconnectEpoch = this.#epoch;
|
|
868
|
+
const reconnectEpoch = this.#disconnectEpochs.get(name) ?? 0;
|
|
784
869
|
if (oldConnection) {
|
|
785
870
|
// Detach onClose to prevent re-entrant reconnect from the close itself
|
|
786
871
|
oldConnection.transport.onClose = undefined;
|
|
787
|
-
|
|
872
|
+
const closePromise = oldConnection.transport.close().catch(() => {});
|
|
873
|
+
if (oldConnection.transport.closeBeforeReconnect) {
|
|
874
|
+
await closePromise;
|
|
875
|
+
} else {
|
|
876
|
+
// Fire-and-forget: don't await HTTP/SSE close — HttpTransport.close()
|
|
877
|
+
// sends a DELETE with config.timeout (30s default), and blocking here
|
|
878
|
+
// delays the reconnect loop by that amount on every server restart.
|
|
879
|
+
void closePromise;
|
|
880
|
+
}
|
|
788
881
|
this.#connections.delete(name);
|
|
789
882
|
}
|
|
790
883
|
this.#pendingConnections.delete(name);
|
|
884
|
+
const backoffAbort = new AbortController();
|
|
885
|
+
this.#reconnectBackoffs.set(name, backoffAbort);
|
|
791
886
|
this.#pendingToolLoads.delete(name);
|
|
792
887
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
storedEpoch: reconnectEpoch,
|
|
800
|
-
currentEpoch: this.#epoch,
|
|
801
|
-
});
|
|
802
|
-
return null;
|
|
803
|
-
}
|
|
804
|
-
try {
|
|
805
|
-
const connection = await this.#connectAndWireServer(name, config, source, reconnectEpoch);
|
|
806
|
-
logger.debug("MCP reconnected", { path: `mcp:${name}`, tools: connection.tools?.length ?? 0 });
|
|
807
|
-
return connection;
|
|
808
|
-
} catch (error) {
|
|
809
|
-
if (this.#epoch !== reconnectEpoch) {
|
|
810
|
-
logger.debug("MCP reconnect aborted after configuration changed", {
|
|
888
|
+
try {
|
|
889
|
+
// Retry with backoff — the server may still be starting up.
|
|
890
|
+
const delays = [500, 1000, 2000, 4000];
|
|
891
|
+
for (let attempt = 0; attempt <= delays.length; attempt++) {
|
|
892
|
+
if ((this.#disconnectEpochs.get(name) ?? 0) !== reconnectEpoch || backoffAbort.signal.aborted) {
|
|
893
|
+
logger.debug("MCP reconnect aborted before attempt after server disconnected", {
|
|
811
894
|
path: `mcp:${name}`,
|
|
812
895
|
storedEpoch: reconnectEpoch,
|
|
813
|
-
currentEpoch: this.#
|
|
896
|
+
currentEpoch: this.#disconnectEpochs.get(name) ?? 0,
|
|
814
897
|
});
|
|
815
898
|
return null;
|
|
816
899
|
}
|
|
900
|
+
try {
|
|
901
|
+
const connection = await this.#connectAndWireServer(name, config, source, this.#epoch, reconnectEpoch);
|
|
902
|
+
logger.debug("MCP reconnected", { path: `mcp:${name}`, tools: connection.tools?.length ?? 0 });
|
|
903
|
+
return connection;
|
|
904
|
+
} catch (error) {
|
|
905
|
+
if ((this.#disconnectEpochs.get(name) ?? 0) !== reconnectEpoch || backoffAbort.signal.aborted) {
|
|
906
|
+
logger.debug("MCP reconnect aborted after server disconnected", {
|
|
907
|
+
path: `mcp:${name}`,
|
|
908
|
+
storedEpoch: reconnectEpoch,
|
|
909
|
+
currentEpoch: this.#disconnectEpochs.get(name) ?? 0,
|
|
910
|
+
});
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
817
913
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
914
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
915
|
+
if (attempt < delays.length) {
|
|
916
|
+
logger.debug("MCP reconnect attempt failed, retrying", {
|
|
917
|
+
path: `mcp:${name}`,
|
|
918
|
+
attempt: attempt + 1,
|
|
919
|
+
error: msg,
|
|
920
|
+
});
|
|
921
|
+
await delay(delays[attempt], backoffAbort.signal).catch(() => undefined);
|
|
922
|
+
} else {
|
|
923
|
+
logger.error("MCP reconnect failed after retries", { path: `mcp:${name}`, error: msg });
|
|
924
|
+
// Don't remove stale tools — keep them in the registry so they
|
|
925
|
+
// remain selected. Calls will fail with MCP errors, which
|
|
926
|
+
// triggers the tool-level reconnect, or the user can run
|
|
927
|
+
// /mcp reconnect <name> manually.
|
|
928
|
+
}
|
|
832
929
|
}
|
|
833
930
|
}
|
|
931
|
+
} finally {
|
|
932
|
+
if (this.#reconnectBackoffs.get(name) === backoffAbort) {
|
|
933
|
+
this.#reconnectBackoffs.delete(name);
|
|
934
|
+
}
|
|
834
935
|
}
|
|
835
936
|
return null;
|
|
836
937
|
}
|
|
@@ -840,7 +941,8 @@ export class MCPManager {
|
|
|
840
941
|
name: string,
|
|
841
942
|
config: MCPServerConfig,
|
|
842
943
|
source: SourceMeta | undefined,
|
|
843
|
-
|
|
944
|
+
globalEpoch: number,
|
|
945
|
+
disconnectEpoch: number,
|
|
844
946
|
): Promise<MCPServerConnection> {
|
|
845
947
|
const resolvedConfig = await this.#resolveAuthConfig(config);
|
|
846
948
|
const connectionAbort = new AbortController();
|
|
@@ -867,7 +969,11 @@ export class MCPManager {
|
|
|
867
969
|
|
|
868
970
|
// Bail out if the server was disconnected or the manager was reset
|
|
869
971
|
// while we were connecting (e.g. /mcp reload called disconnectAll).
|
|
870
|
-
if (
|
|
972
|
+
if (
|
|
973
|
+
!this.#serverConfigs.has(name) ||
|
|
974
|
+
this.#epoch !== globalEpoch ||
|
|
975
|
+
(this.#disconnectEpochs.get(name) ?? 0) !== disconnectEpoch
|
|
976
|
+
) {
|
|
871
977
|
await connection.transport.close().catch(() => {});
|
|
872
978
|
throw new Error(`Server "${name}" was disconnected during reconnection`);
|
|
873
979
|
}
|
|
@@ -890,6 +996,11 @@ export class MCPManager {
|
|
|
890
996
|
};
|
|
891
997
|
try {
|
|
892
998
|
const serverTools = await listTools(connection);
|
|
999
|
+
if (!this.#isCurrentConnection(name, config, globalEpoch, disconnectEpoch, connection)) {
|
|
1000
|
+
connection.transport.onClose = undefined;
|
|
1001
|
+
await connection.transport.close().catch(() => {});
|
|
1002
|
+
throw new Error(`Server "${name}" was disconnected during tool loading`);
|
|
1003
|
+
}
|
|
893
1004
|
const reconnect = () => this.reconnectServer(name);
|
|
894
1005
|
const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
|
|
895
1006
|
void this.toolCache?.set(name, config, serverTools);
|
|
@@ -901,7 +1012,7 @@ export class MCPManager {
|
|
|
901
1012
|
// Clean up the connection to avoid zombie transports
|
|
902
1013
|
connection.transport.onClose = undefined;
|
|
903
1014
|
await connection.transport.close().catch(() => {});
|
|
904
|
-
this.#connections.delete(name);
|
|
1015
|
+
if (this.#connections.get(name) === connection) this.#connections.delete(name);
|
|
905
1016
|
throw error;
|
|
906
1017
|
}
|
|
907
1018
|
}
|
|
@@ -941,12 +1052,15 @@ export class MCPManager {
|
|
|
941
1052
|
async refreshServerTools(name: string): Promise<void> {
|
|
942
1053
|
const connection = this.#connections.get(name);
|
|
943
1054
|
if (!connection) return;
|
|
1055
|
+
const globalEpoch = this.#epoch;
|
|
1056
|
+
const disconnectEpoch = this.#disconnectEpochs.get(name) ?? 0;
|
|
944
1057
|
|
|
945
1058
|
// Clear cached tools
|
|
946
1059
|
connection.tools = undefined;
|
|
947
1060
|
|
|
948
1061
|
// Reload tools
|
|
949
1062
|
const serverTools = await listTools(connection);
|
|
1063
|
+
if (!this.#isCurrentConnection(name, connection.config, globalEpoch, disconnectEpoch, connection)) return;
|
|
950
1064
|
const reconnect = () => this.reconnectServer(name);
|
|
951
1065
|
const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
|
|
952
1066
|
void this.toolCache?.set(name, connection.config, serverTools);
|
|
@@ -99,6 +99,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
if (response.status === 405 || !response.ok || !response.body) {
|
|
102
|
+
await response.body?.cancel().catch(() => {});
|
|
102
103
|
this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
@@ -209,8 +210,6 @@ export class HttpTransport implements MCPTransport {
|
|
|
209
210
|
signal: operationSignal,
|
|
210
211
|
});
|
|
211
212
|
|
|
212
|
-
clearTimeout(timeoutId);
|
|
213
|
-
|
|
214
213
|
// Check for session ID in response
|
|
215
214
|
const newSessionId = response.headers.get("Mcp-Session-Id");
|
|
216
215
|
if (newSessionId) {
|
|
@@ -247,7 +246,6 @@ export class HttpTransport implements MCPTransport {
|
|
|
247
246
|
|
|
248
247
|
return result.result as T;
|
|
249
248
|
} catch (error) {
|
|
250
|
-
clearTimeout(timeoutId);
|
|
251
249
|
if (error instanceof Error && error.name === "AbortError") {
|
|
252
250
|
if (options?.signal?.aborted) {
|
|
253
251
|
throw error;
|
|
@@ -255,6 +253,8 @@ export class HttpTransport implements MCPTransport {
|
|
|
255
253
|
throw new Error(`Request timeout after ${timeout}ms`);
|
|
256
254
|
}
|
|
257
255
|
throw error;
|
|
256
|
+
} finally {
|
|
257
|
+
clearTimeout(timeoutId);
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
@@ -273,12 +273,12 @@ export class HttpTransport implements MCPTransport {
|
|
|
273
273
|
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
274
274
|
let captured = false;
|
|
275
275
|
|
|
276
|
-
// Drain
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// Re-reading `response.body`
|
|
280
|
-
//
|
|
281
|
-
//
|
|
276
|
+
// Drain this per-request SSE response from a single iterator. Once the
|
|
277
|
+
// matching response arrives, resolve/reject and abort the reader so the
|
|
278
|
+
// response body is cancelled instead of lingering in the background.
|
|
279
|
+
// Re-reading `response.body` would lock the stream a second time and surface
|
|
280
|
+
// as "ReadableStream already has a controller", so the iterator owns the
|
|
281
|
+
// stream until it is aborted, completes, or errors.
|
|
282
282
|
const drainController = abortController;
|
|
283
283
|
this.#streamControllers.add(drainController);
|
|
284
284
|
const drain = async (): Promise<void> => {
|
|
@@ -293,13 +293,13 @@ export class HttpTransport implements MCPTransport {
|
|
|
293
293
|
("result" in message || "error" in message)
|
|
294
294
|
) {
|
|
295
295
|
captured = true;
|
|
296
|
-
|
|
296
|
+
drainController.abort();
|
|
297
297
|
if (message.error) {
|
|
298
298
|
reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`));
|
|
299
299
|
} else {
|
|
300
300
|
resolve(message.result as T);
|
|
301
301
|
}
|
|
302
|
-
|
|
302
|
+
return;
|
|
303
303
|
}
|
|
304
304
|
if (!this.#connected) continue;
|
|
305
305
|
this.#dispatchSSEMessage(message);
|
|
@@ -322,6 +322,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
322
322
|
} finally {
|
|
323
323
|
clearTimeout(timeoutId);
|
|
324
324
|
this.#streamControllers.delete(drainController);
|
|
325
|
+
await response.body?.cancel().catch(() => {});
|
|
325
326
|
}
|
|
326
327
|
};
|
|
327
328
|
|