@gajae-code/coding-agent 0.16.3 → 0.16.6
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 +113 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +35 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +26 -1
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +112 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +10 -0
- package/dist/types/task/executor.d.ts +5 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +10 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +9 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +11 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli/root-flags.ts +5 -3
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +379 -31
- package/src/config/model-preset-registry.ts +120 -19
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +73 -23
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +52 -55
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +307 -34
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +228 -60
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +149 -190
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +73 -25
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +143 -10
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +12 -12
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +78 -35
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +396 -22
- package/src/modes/controllers/command-controller.ts +3 -0
- package/src/modes/controllers/event-controller.ts +151 -7
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +53 -6
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +17 -3
- package/src/modes/utils/ui-helpers.ts +2 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +189 -14
- package/src/runtime-mcp/pool.test.ts +218 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/broker/broker.ts +124 -32
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +478 -49
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +11 -8
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +52 -10
- package/src/sdk/host/session-runtime.ts +383 -44
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +77 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +228 -55
- package/src/sdk/transport/relay.ts +35 -13
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +240 -90
- package/src/session/contribution-prep.ts +214 -21
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +456 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +29 -3
- package/src/task/executor.ts +7 -1
- package/src/task/index.ts +21 -4
- package/src/tools/bash.ts +63 -19
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +10 -0
- package/src/tools/job.ts +21 -4
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/search.ts +116 -58
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +37 -2
- package/src/tools/subagent.ts +6 -3
- package/src/tools/tool-catalog.generated.ts +10 -3
- package/src/tools/ultragoal-ask-guard.ts +2 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { MCPConnectionCleanupFailure } from "./client";
|
|
3
|
+
import { MCPManager } from "./manager";
|
|
2
4
|
import { MCPConnectionPool, MCPPoolAcquireAbortError, MCPPoolLeaseInvalidatedError } from "./pool";
|
|
3
5
|
import { MCPPoolConfigError } from "./pool-key";
|
|
4
6
|
import { legacyEraObservation } from "./protocol";
|
|
@@ -48,6 +50,17 @@ class FakeTransport implements MCPTransport {
|
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
class ToolListTransport extends FakeTransport {
|
|
54
|
+
override async request<T = unknown>(
|
|
55
|
+
method: string,
|
|
56
|
+
params?: Record<string, unknown>,
|
|
57
|
+
options?: MCPRequestOptions,
|
|
58
|
+
): Promise<T> {
|
|
59
|
+
if (method === "tools/list") return { tools: [] } as T;
|
|
60
|
+
return super.request<T>(method, params, options);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
class DelayedSubscriptionTransport extends FakeTransport {
|
|
52
65
|
readonly subscribeStarted = Promise.withResolvers<void>();
|
|
53
66
|
readonly allowSubscribe = Promise.withResolvers<void>();
|
|
@@ -373,6 +386,211 @@ describe("MCPConnectionPool", () => {
|
|
|
373
386
|
await pool.shutdown();
|
|
374
387
|
});
|
|
375
388
|
|
|
389
|
+
test("retains cancelled cleanup before late failure and fences each cleanup generation", async () => {
|
|
390
|
+
const flights = [Promise.withResolvers<MCPServerConnection>(), Promise.withResolvers<MCPServerConnection>()];
|
|
391
|
+
const starts = [Promise.withResolvers<void>(), Promise.withResolvers<void>()];
|
|
392
|
+
const recoveryTransports: FakeTransport[] = [];
|
|
393
|
+
let opens = 0;
|
|
394
|
+
const pool = new MCPConnectionPool({
|
|
395
|
+
connect: async (name, cfg) => {
|
|
396
|
+
opens += 1;
|
|
397
|
+
if (opens === 1) {
|
|
398
|
+
starts[0]?.resolve();
|
|
399
|
+
return flights[0]!.promise;
|
|
400
|
+
}
|
|
401
|
+
if (opens === 3) {
|
|
402
|
+
starts[1]?.resolve();
|
|
403
|
+
return flights[1]!.promise;
|
|
404
|
+
}
|
|
405
|
+
const transport = new FakeTransport();
|
|
406
|
+
recoveryTransports.push(transport);
|
|
407
|
+
return connection(name, cfg, transport);
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
const acquireCancelledFlight = async (index: number): Promise<MCPPoolAcquireAbortError> => {
|
|
411
|
+
const controller = new AbortController();
|
|
412
|
+
const acquire = pool.acquire("server", config(), {
|
|
413
|
+
sessionId: "retained-cleanup",
|
|
414
|
+
signal: controller.signal,
|
|
415
|
+
});
|
|
416
|
+
await starts[index]!.promise;
|
|
417
|
+
controller.abort(new Error(`caller abort ${index}`));
|
|
418
|
+
const error = await acquire.catch(error => error);
|
|
419
|
+
expect(error).toBeInstanceOf(MCPPoolAcquireAbortError);
|
|
420
|
+
if (!(error instanceof MCPPoolAcquireAbortError)) throw new Error("Expected aborted pool acquisition");
|
|
421
|
+
return error;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const firstAbort = await acquireCancelledFlight(0);
|
|
425
|
+
expect(pool.pendingAcquireCleanupCountForTests).toBe(1);
|
|
426
|
+
expect(opens).toBe(1);
|
|
427
|
+
const firstCleanupTransport = new FakeTransport();
|
|
428
|
+
let fencedSettled = false;
|
|
429
|
+
const fenced = pool.acquire("server", config(), { sessionId: "retained-cleanup" });
|
|
430
|
+
void fenced.then(
|
|
431
|
+
() => {
|
|
432
|
+
fencedSettled = true;
|
|
433
|
+
},
|
|
434
|
+
() => {
|
|
435
|
+
fencedSettled = true;
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
await Promise.resolve();
|
|
439
|
+
expect(fencedSettled).toBe(false);
|
|
440
|
+
expect(opens).toBe(1);
|
|
441
|
+
flights[0]!.reject(new MCPConnectionCleanupFailure(new Error("late cleanup failure 1"), firstCleanupTransport));
|
|
442
|
+
await expect(fenced).resolves.toBeDefined();
|
|
443
|
+
expect(firstCleanupTransport.closeCount).toBe(1);
|
|
444
|
+
expect(opens).toBe(2);
|
|
445
|
+
|
|
446
|
+
const secondLease = await fenced;
|
|
447
|
+
await secondLease.release();
|
|
448
|
+
const secondAbort = await acquireCancelledFlight(1);
|
|
449
|
+
expect(secondAbort.cleanupGeneration).toBeGreaterThan(firstAbort.cleanupGeneration ?? 0);
|
|
450
|
+
const secondCleanupTransport = new FakeTransport();
|
|
451
|
+
flights[1]!.reject(new MCPConnectionCleanupFailure(new Error("late cleanup failure 2"), secondCleanupTransport));
|
|
452
|
+
await expect(secondAbort.cleanup).rejects.toBeInstanceOf(MCPConnectionCleanupFailure);
|
|
453
|
+
await pool.closePendingAcquireCleanup(firstAbort.poolKey, firstAbort.cleanupGeneration!);
|
|
454
|
+
expect(secondCleanupTransport.closeCount).toBe(0);
|
|
455
|
+
expect(pool.pendingAcquireCleanupCountForTests).toBe(1);
|
|
456
|
+
await pool.closePendingAcquireCleanup(secondAbort.poolKey, secondAbort.cleanupGeneration!);
|
|
457
|
+
expect(secondCleanupTransport.closeCount).toBe(1);
|
|
458
|
+
expect(pool.pendingAcquireCleanupCountForTests).toBe(0);
|
|
459
|
+
const recovered = await pool.acquire("server", config(), { sessionId: "retained-cleanup" });
|
|
460
|
+
expect(opens).toBe(4);
|
|
461
|
+
await recovered.release();
|
|
462
|
+
expect(recoveryTransports).toHaveLength(2);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("manager disconnect preserves late cleanup failure until pool retry clears every manager fence", async () => {
|
|
466
|
+
const firstFlight = Promise.withResolvers<MCPServerConnection>();
|
|
467
|
+
const firstStarted = Promise.withResolvers<void>();
|
|
468
|
+
const cleanupTransport = new FakeTransport();
|
|
469
|
+
const retryFailure = new Error("deferred cleanup retry failed");
|
|
470
|
+
let cleanupAttempts = 0;
|
|
471
|
+
cleanupTransport.close = async () => {
|
|
472
|
+
cleanupAttempts += 1;
|
|
473
|
+
if (cleanupAttempts === 1) throw retryFailure;
|
|
474
|
+
cleanupTransport.connected = false;
|
|
475
|
+
};
|
|
476
|
+
let opens = 0;
|
|
477
|
+
const pool = new MCPConnectionPool({
|
|
478
|
+
connect: async (name, cfg) => {
|
|
479
|
+
opens += 1;
|
|
480
|
+
if (opens === 1) {
|
|
481
|
+
firstStarted.resolve();
|
|
482
|
+
return firstFlight.promise;
|
|
483
|
+
}
|
|
484
|
+
return connection(name, cfg, new ToolListTransport());
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
const manager = new MCPManager(".", null, { pool, sessionId: "disconnect-cleanup" });
|
|
488
|
+
const serverConfig = config();
|
|
489
|
+
const controller = new AbortController();
|
|
490
|
+
const operation = manager.withPreparedLease("server", serverConfig, async () => undefined, {
|
|
491
|
+
signal: controller.signal,
|
|
492
|
+
});
|
|
493
|
+
await firstStarted.promise;
|
|
494
|
+
controller.abort(new Error("abort before deferred cleanup failure"));
|
|
495
|
+
const abortedAcquire = await operation.catch(error => error);
|
|
496
|
+
expect(abortedAcquire).toBeInstanceOf(MCPPoolAcquireAbortError);
|
|
497
|
+
if (!(abortedAcquire instanceof MCPPoolAcquireAbortError)) throw new Error("Expected aborted acquisition");
|
|
498
|
+
expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
|
|
499
|
+
|
|
500
|
+
await manager.disconnectServer("server");
|
|
501
|
+
const fenced = await manager.connectServers({ server: serverConfig }, {});
|
|
502
|
+
expect(fenced.errors.has("server")).toBe(true);
|
|
503
|
+
expect(opens).toBe(1);
|
|
504
|
+
const primaryFailure = new Error("late connection cleanup failed");
|
|
505
|
+
firstFlight.reject(new MCPConnectionCleanupFailure(primaryFailure, cleanupTransport));
|
|
506
|
+
await expect(abortedAcquire.cleanup).rejects.toBeInstanceOf(MCPConnectionCleanupFailure);
|
|
507
|
+
const disconnectError = await manager.disconnectServer("server").catch(error => error);
|
|
508
|
+
expect(disconnectError).toBeInstanceOf(MCPConnectionCleanupFailure);
|
|
509
|
+
if (!(disconnectError instanceof MCPConnectionCleanupFailure)) {
|
|
510
|
+
throw new Error("Expected retained disconnect cleanup failure");
|
|
511
|
+
}
|
|
512
|
+
expect(disconnectError.cause).toBeInstanceOf(AggregateError);
|
|
513
|
+
if (!(disconnectError.cause instanceof AggregateError)) throw new Error("Expected combined cleanup causes");
|
|
514
|
+
expect(disconnectError.cause.errors).toEqual([primaryFailure, retryFailure]);
|
|
515
|
+
expect(cleanupAttempts).toBe(1);
|
|
516
|
+
expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
|
|
517
|
+
expect(pool.pendingAcquireCleanupCountForTests).toBe(1);
|
|
518
|
+
|
|
519
|
+
const recoveryManager = new MCPManager(".", null, { pool, sessionId: "disconnect-cleanup" });
|
|
520
|
+
await recoveryManager.withPreparedLease("server", serverConfig, async lease => {
|
|
521
|
+
expect(lease.serverName).toBe("server");
|
|
522
|
+
});
|
|
523
|
+
expect(cleanupAttempts).toBe(2);
|
|
524
|
+
expect(pool.pendingAcquireCleanupCountForTests).toBe(0);
|
|
525
|
+
expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
|
|
526
|
+
|
|
527
|
+
const recovered = await manager.connectServers({ server: serverConfig }, {});
|
|
528
|
+
expect(recovered.errors.size).toBe(0);
|
|
529
|
+
expect(recovered.connectedServers).toEqual(["server"]);
|
|
530
|
+
expect(manager.pendingConnectionCleanupCountForTests).toBe(0);
|
|
531
|
+
expect(opens).toBe(3);
|
|
532
|
+
await recoveryManager.disconnectAll();
|
|
533
|
+
await manager.disconnectAll();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("manager reconnect retries pool-owned cleanup before replacing the live connection", async () => {
|
|
537
|
+
const transientFlight = Promise.withResolvers<MCPServerConnection>();
|
|
538
|
+
const transientStarted = Promise.withResolvers<void>();
|
|
539
|
+
const cleanupTransport = new FakeTransport();
|
|
540
|
+
const cleanupRetryFailure = new Error("reconnect cleanup retry failed");
|
|
541
|
+
let cleanupAttempts = 0;
|
|
542
|
+
cleanupTransport.close = async () => {
|
|
543
|
+
cleanupAttempts += 1;
|
|
544
|
+
if (cleanupAttempts === 1) throw cleanupRetryFailure;
|
|
545
|
+
cleanupTransport.connected = false;
|
|
546
|
+
};
|
|
547
|
+
let opens = 0;
|
|
548
|
+
const transports: ToolListTransport[] = [];
|
|
549
|
+
const pool = new MCPConnectionPool({
|
|
550
|
+
connect: async (name, cfg) => {
|
|
551
|
+
opens += 1;
|
|
552
|
+
if (opens === 2) {
|
|
553
|
+
transientStarted.resolve();
|
|
554
|
+
return transientFlight.promise;
|
|
555
|
+
}
|
|
556
|
+
const transport = new ToolListTransport();
|
|
557
|
+
transports.push(transport);
|
|
558
|
+
return connection(name, cfg, transport);
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
const manager = new MCPManager(".", null, { pool, sessionId: "reconnect-cleanup", sleep: async () => {} });
|
|
562
|
+
const serverConfig = config();
|
|
563
|
+
await manager.connectServers({ server: serverConfig }, {});
|
|
564
|
+
const original = manager.getConnection("server");
|
|
565
|
+
const controller = new AbortController();
|
|
566
|
+
const transient = manager.withPreparedLease(
|
|
567
|
+
"server",
|
|
568
|
+
{ type: "stdio", command: "fake-mcp", args: ["--transient"] },
|
|
569
|
+
async () => undefined,
|
|
570
|
+
{ signal: controller.signal },
|
|
571
|
+
);
|
|
572
|
+
await transientStarted.promise;
|
|
573
|
+
controller.abort(new Error("abort transient before reconnect"));
|
|
574
|
+
await expect(transient).rejects.toBeInstanceOf(MCPPoolAcquireAbortError);
|
|
575
|
+
|
|
576
|
+
const firstReconnect = manager.reconnectServer("server");
|
|
577
|
+
transientFlight.reject(
|
|
578
|
+
new MCPConnectionCleanupFailure(new Error("late reconnect cleanup failed"), cleanupTransport),
|
|
579
|
+
);
|
|
580
|
+
await expect(firstReconnect).rejects.toBeInstanceOf(MCPConnectionCleanupFailure);
|
|
581
|
+
expect(cleanupAttempts).toBe(1);
|
|
582
|
+
expect(opens).toBe(2);
|
|
583
|
+
expect(manager.getConnection("server")).toBe(original);
|
|
584
|
+
const replacement = await manager.reconnectServer("server");
|
|
585
|
+
expect(cleanupAttempts).toBe(2);
|
|
586
|
+
expect(opens).toBe(3);
|
|
587
|
+
expect(replacement).toBeDefined();
|
|
588
|
+
expect(replacement).not.toBe(original);
|
|
589
|
+
expect(manager.pendingConnectionCleanupCountForTests).toBe(0);
|
|
590
|
+
await manager.disconnectAll();
|
|
591
|
+
expect(transports.every(transport => transport.closeCount === 1)).toBe(true);
|
|
592
|
+
});
|
|
593
|
+
|
|
376
594
|
test("shutdown aborts hanging opens, settles waiters, and closes late transports", async () => {
|
|
377
595
|
let resolveOpen: ((value: MCPServerConnection) => void) | undefined;
|
|
378
596
|
let openSignal: AbortSignal | undefined;
|
package/src/runtime-mcp/pool.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* multiple leases and roots/resource subscriptions are connection-global unions.
|
|
6
6
|
*/
|
|
7
7
|
import { logger } from "@gajae-code/utils";
|
|
8
|
-
import { connectToServer, subscribeToResources, unsubscribeFromResources } from "./client";
|
|
8
|
+
import { connectToServer, MCPConnectionCleanupFailure, subscribeToResources, unsubscribeFromResources } from "./client";
|
|
9
9
|
import {
|
|
10
10
|
buildMCPPoolKeyIdentity,
|
|
11
11
|
computeMCPPoolKey,
|
|
@@ -60,9 +60,16 @@ export class MCPPoolAcquireAbortError extends Error {
|
|
|
60
60
|
readonly code = "MCP_POOL_ACQUIRE_ABORTED" as const;
|
|
61
61
|
readonly serverName: string;
|
|
62
62
|
readonly poolKey: string;
|
|
63
|
-
cleanup?: Promise<
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
readonly cleanup?: Promise<void>;
|
|
64
|
+
readonly cleanupGeneration?: number;
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
serverName: string,
|
|
68
|
+
poolKey: string,
|
|
69
|
+
cause: unknown,
|
|
70
|
+
cleanup?: Promise<void>,
|
|
71
|
+
cleanupGeneration?: number,
|
|
72
|
+
) {
|
|
66
73
|
super(
|
|
67
74
|
`MCP connection acquisition aborted for ${serverName} (${poolKey}): ${
|
|
68
75
|
cause instanceof Error ? cause.message : String(cause)
|
|
@@ -73,6 +80,7 @@ export class MCPPoolAcquireAbortError extends Error {
|
|
|
73
80
|
this.serverName = serverName;
|
|
74
81
|
this.poolKey = poolKey;
|
|
75
82
|
this.cleanup = cleanup;
|
|
83
|
+
this.cleanupGeneration = cleanupGeneration;
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
@@ -121,6 +129,8 @@ export interface MCPPoolAcquireOptions extends MCPPoolKeyOptions {
|
|
|
121
129
|
advertiseRoots?: boolean;
|
|
122
130
|
onNotification?: (method: string, params: unknown) => void;
|
|
123
131
|
onRequest?: (method: string, params: unknown) => Promise<unknown>;
|
|
132
|
+
/** @internal Synchronous handoff for a pool-owned abandoned-acquire cleanup flight. */
|
|
133
|
+
onCleanupPending?: (error: MCPPoolAcquireAbortError) => void;
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
export interface MCPConnectionPoolOptions {
|
|
@@ -172,6 +182,7 @@ type PendingWaiter = {
|
|
|
172
182
|
settled: boolean;
|
|
173
183
|
claimed: boolean;
|
|
174
184
|
removeAbortListener?: () => void;
|
|
185
|
+
onCleanupPending?: (error: MCPPoolAcquireAbortError) => void;
|
|
175
186
|
};
|
|
176
187
|
|
|
177
188
|
type PendingEntry = {
|
|
@@ -186,7 +197,17 @@ type PendingEntry = {
|
|
|
186
197
|
cancellationReason?: unknown;
|
|
187
198
|
openAbortController: AbortController;
|
|
188
199
|
openSignal?: AbortSignal;
|
|
189
|
-
settlement?: Promise<
|
|
200
|
+
settlement?: Promise<void>;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
type RetiredAcquisitionCleanup = {
|
|
204
|
+
key: string;
|
|
205
|
+
serverName: string;
|
|
206
|
+
generation: number;
|
|
207
|
+
completion: Promise<void>;
|
|
208
|
+
error: MCPPoolAcquireAbortError;
|
|
209
|
+
failure?: MCPConnectionCleanupFailure;
|
|
210
|
+
retry?: Promise<void>;
|
|
190
211
|
};
|
|
191
212
|
|
|
192
213
|
function errorMessage(error: unknown): string {
|
|
@@ -386,6 +407,7 @@ export class MCPConnectionPool {
|
|
|
386
407
|
readonly #pending = new Map<string, PendingEntry>();
|
|
387
408
|
readonly #allLeases = new Set<MCPPoolLeaseImpl>();
|
|
388
409
|
readonly #retiredEntries = new Set<PoolEntry>();
|
|
410
|
+
readonly #retiredAcquisitionCleanups = new Map<string, RetiredAcquisitionCleanup>();
|
|
389
411
|
readonly #restartOwners = new Set<string>();
|
|
390
412
|
readonly #restartWaiters = new Map<string, { promise: Promise<boolean>; resolve: (value: boolean) => void }>();
|
|
391
413
|
readonly #healthListeners = new Set<HealthListener>();
|
|
@@ -458,22 +480,160 @@ export class MCPConnectionPool {
|
|
|
458
480
|
this.#restartWaiters.delete(key);
|
|
459
481
|
}
|
|
460
482
|
|
|
483
|
+
private retainCancelledAcquireCleanup(
|
|
484
|
+
key: string,
|
|
485
|
+
name: string,
|
|
486
|
+
pending: PendingEntry,
|
|
487
|
+
cause: unknown,
|
|
488
|
+
): RetiredAcquisitionCleanup {
|
|
489
|
+
const current = this.#retiredAcquisitionCleanups.get(key);
|
|
490
|
+
if (current) return current;
|
|
491
|
+
const completion = pending.settlement;
|
|
492
|
+
const generation = pending.entry?.generation;
|
|
493
|
+
if (!completion || generation === undefined) {
|
|
494
|
+
throw new Error(`MCP cancelled acquisition cleanup is missing its flight identity: ${key}`);
|
|
495
|
+
}
|
|
496
|
+
const error = new MCPPoolAcquireAbortError(name, key, cause, completion, generation);
|
|
497
|
+
const owner: RetiredAcquisitionCleanup = { key, serverName: name, generation, completion, error };
|
|
498
|
+
this.#retiredAcquisitionCleanups.set(key, owner);
|
|
499
|
+
void completion.then(
|
|
500
|
+
() => {
|
|
501
|
+
if (this.#retiredAcquisitionCleanups.get(key) === owner && !owner.retry) {
|
|
502
|
+
this.#retiredAcquisitionCleanups.delete(key);
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
error => {
|
|
506
|
+
if (error instanceof MCPConnectionCleanupFailure) {
|
|
507
|
+
owner.failure = error;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (this.#retiredAcquisitionCleanups.get(key) === owner && !owner.retry) {
|
|
511
|
+
this.#retiredAcquisitionCleanups.delete(key);
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
return owner;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private notifyPendingAcquireCleanup(
|
|
519
|
+
listener: MCPPoolAcquireOptions["onCleanupPending"],
|
|
520
|
+
owner: RetiredAcquisitionCleanup,
|
|
521
|
+
): void {
|
|
522
|
+
try {
|
|
523
|
+
listener?.(owner.error);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
logger.debug("MCP cancelled acquire cleanup handoff failed", {
|
|
526
|
+
path: `mcp:${owner.serverName}`,
|
|
527
|
+
poolKey: owner.key,
|
|
528
|
+
error,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** @internal Whether this pool still owns the notified abandoned-acquire flight. */
|
|
534
|
+
hasPendingAcquireCleanup(error: MCPPoolAcquireAbortError): boolean {
|
|
535
|
+
const owner = this.#retiredAcquisitionCleanups.get(error.poolKey);
|
|
536
|
+
return owner?.generation === error.cleanupGeneration;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** @internal Current retained failure for one notified abandoned-acquire generation. */
|
|
540
|
+
getPendingAcquireCleanupFailure(error: MCPPoolAcquireAbortError): MCPConnectionCleanupFailure | undefined {
|
|
541
|
+
const owner = this.#retiredAcquisitionCleanups.get(error.poolKey);
|
|
542
|
+
return owner && owner.generation === error.cleanupGeneration ? owner.failure : undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** @internal Prevent manager-level duplication of the pool's retained cleanup owner. */
|
|
546
|
+
ownsPendingAcquireCleanupFailure(error: unknown): boolean {
|
|
547
|
+
if (!(error instanceof MCPConnectionCleanupFailure)) return false;
|
|
548
|
+
for (const owner of this.#retiredAcquisitionCleanups.values()) {
|
|
549
|
+
if (owner.failure === error) return true;
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Read-only test seam for abandoned-acquire cleanup owners. */
|
|
555
|
+
get pendingAcquireCleanupCountForTests(): number {
|
|
556
|
+
return this.#retiredAcquisitionCleanups.size;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Read-only test seam for the current retained abandoned-acquire cleanup failure. */
|
|
560
|
+
getPendingAcquireCleanupFailureForTests(name: string): MCPConnectionCleanupFailure | undefined {
|
|
561
|
+
for (const owner of this.#retiredAcquisitionCleanups.values()) {
|
|
562
|
+
if (owner.serverName === name && owner.failure) return owner.failure;
|
|
563
|
+
}
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** @internal Join and retry one generation of abandoned acquisition cleanup. */
|
|
568
|
+
async closePendingAcquireCleanup(key: string, generation: number): Promise<void> {
|
|
569
|
+
const owner = this.#retiredAcquisitionCleanups.get(key);
|
|
570
|
+
if (!owner || owner.generation !== generation) return;
|
|
571
|
+
if (owner.retry) return owner.retry;
|
|
572
|
+
let tracked!: Promise<void>;
|
|
573
|
+
tracked = (async () => {
|
|
574
|
+
let failure = owner.failure;
|
|
575
|
+
if (!failure) {
|
|
576
|
+
try {
|
|
577
|
+
await owner.completion;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (!(error instanceof MCPConnectionCleanupFailure)) throw error;
|
|
580
|
+
failure = owner.failure ?? error;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (failure) {
|
|
584
|
+
try {
|
|
585
|
+
await failure.close();
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (this.#retiredAcquisitionCleanups.get(key) === owner) {
|
|
588
|
+
owner.failure = error instanceof MCPConnectionCleanupFailure ? error : failure;
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (this.#retiredAcquisitionCleanups.get(key) === owner) this.#retiredAcquisitionCleanups.delete(key);
|
|
594
|
+
})().finally(() => {
|
|
595
|
+
if (this.#retiredAcquisitionCleanups.get(key) === owner && owner.retry === tracked) owner.retry = undefined;
|
|
596
|
+
});
|
|
597
|
+
owner.retry = tracked;
|
|
598
|
+
return tracked;
|
|
599
|
+
}
|
|
600
|
+
|
|
461
601
|
async acquire(name: string, config: MCPServerConfig, options: MCPPoolAcquireOptions = {}): Promise<MCPPoolLease> {
|
|
462
602
|
if (this.#shuttingDown) throw new Error("MCP connection pool is shut down");
|
|
463
603
|
const keyOptions: MCPPoolKeyOptions = options;
|
|
464
604
|
const key = computeMCPPoolKey(name, config, keyOptions);
|
|
605
|
+
if (options.signal?.aborted) {
|
|
606
|
+
throw new MCPPoolAcquireAbortError(
|
|
607
|
+
name,
|
|
608
|
+
key,
|
|
609
|
+
options.signal.reason ?? new Error(`MCP connection acquisition aborted: ${name}`),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
for (;;) {
|
|
613
|
+
const cleanup = this.#retiredAcquisitionCleanups.get(key);
|
|
614
|
+
if (!cleanup) break;
|
|
615
|
+
this.notifyPendingAcquireCleanup(options.onCleanupPending, cleanup);
|
|
616
|
+
await this.closePendingAcquireCleanup(cleanup.key, cleanup.generation);
|
|
617
|
+
}
|
|
618
|
+
if (options.signal?.aborted) {
|
|
619
|
+
throw new MCPPoolAcquireAbortError(
|
|
620
|
+
name,
|
|
621
|
+
key,
|
|
622
|
+
options.signal.reason ?? new Error(`MCP connection acquisition aborted: ${name}`),
|
|
623
|
+
);
|
|
624
|
+
}
|
|
465
625
|
|
|
466
626
|
let entry = this.#entries.get(key);
|
|
467
627
|
let claim: () => void = () => {};
|
|
468
628
|
if (entry?.pending) {
|
|
469
|
-
const acquisition = await this.waitForPendingEntry(key, name, entry.pending, options
|
|
629
|
+
const acquisition = await this.waitForPendingEntry(key, name, entry.pending, options);
|
|
470
630
|
entry = acquisition.entry;
|
|
471
631
|
claim = acquisition.claim;
|
|
472
632
|
}
|
|
473
633
|
if (!entry) {
|
|
474
634
|
let pending = this.#pending.get(key);
|
|
475
635
|
if (!pending) pending = this.startPendingEntry(key, name, config, options);
|
|
476
|
-
const acquisition = await this.waitForPendingEntry(key, name, pending, options
|
|
636
|
+
const acquisition = await this.waitForPendingEntry(key, name, pending, options);
|
|
477
637
|
entry = acquisition.entry;
|
|
478
638
|
claim = acquisition.claim;
|
|
479
639
|
}
|
|
@@ -502,10 +662,17 @@ export class MCPConnectionPool {
|
|
|
502
662
|
key: string,
|
|
503
663
|
name: string,
|
|
504
664
|
pending: PendingEntry,
|
|
505
|
-
signal
|
|
665
|
+
options: Pick<MCPPoolAcquireOptions, "signal" | "onCleanupPending">,
|
|
506
666
|
): Promise<PendingAcquisition> {
|
|
507
667
|
const { promise, resolve, reject } = Promise.withResolvers<PendingAcquisition>();
|
|
508
|
-
const
|
|
668
|
+
const signal = options.signal;
|
|
669
|
+
const waiter: PendingWaiter = {
|
|
670
|
+
resolve,
|
|
671
|
+
reject,
|
|
672
|
+
settled: false,
|
|
673
|
+
claimed: false,
|
|
674
|
+
onCleanupPending: options.onCleanupPending,
|
|
675
|
+
};
|
|
509
676
|
pending.waiters.add(waiter);
|
|
510
677
|
const settle = (fn: () => void): void => {
|
|
511
678
|
if (waiter.settled) return;
|
|
@@ -515,14 +682,21 @@ export class MCPConnectionPool {
|
|
|
515
682
|
fn();
|
|
516
683
|
};
|
|
517
684
|
const abortWaiter = (): void => {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
685
|
+
if (waiter.settled) return;
|
|
686
|
+
waiter.settled = true;
|
|
687
|
+
pending.waiters.delete(waiter);
|
|
688
|
+
waiter.removeAbortListener?.();
|
|
689
|
+
const cause = signal?.reason ?? new Error(`MCP connection acquisition aborted: ${name}`);
|
|
690
|
+
let reason: MCPPoolAcquireAbortError;
|
|
691
|
+
if (pending.waiters.size === 0 && !pending.settled) {
|
|
692
|
+
const cleanup = this.retainCancelledAcquireCleanup(key, name, pending, cause);
|
|
693
|
+
reason = cleanup.error;
|
|
694
|
+
this.cancelPendingEntry(key, pending, reason);
|
|
695
|
+
this.notifyPendingAcquireCleanup(waiter.onCleanupPending, cleanup);
|
|
696
|
+
} else {
|
|
697
|
+
reason = new MCPPoolAcquireAbortError(name, key, cause);
|
|
698
|
+
}
|
|
699
|
+
reject(reason);
|
|
526
700
|
};
|
|
527
701
|
if (signal) {
|
|
528
702
|
if (signal.aborted) abortWaiter();
|
|
@@ -618,12 +792,22 @@ export class MCPConnectionPool {
|
|
|
618
792
|
try {
|
|
619
793
|
await this.closeEntry(entry);
|
|
620
794
|
} catch (error) {
|
|
621
|
-
|
|
795
|
+
throw new MCPConnectionCleanupFailure(
|
|
796
|
+
new AggregateError(
|
|
797
|
+
[
|
|
798
|
+
pending.cancellationReason ?? new Error(`MCP connection acquisition abandoned: ${name}`),
|
|
799
|
+
error,
|
|
800
|
+
],
|
|
801
|
+
`MCP cancelled acquisition cleanup failed: ${name}`,
|
|
802
|
+
),
|
|
803
|
+
entry.connection.transport,
|
|
804
|
+
);
|
|
622
805
|
}
|
|
623
806
|
},
|
|
624
807
|
error => {
|
|
625
808
|
pending.settled = true;
|
|
626
809
|
pending.reject(error);
|
|
810
|
+
if (pending.cancelled && error instanceof MCPConnectionCleanupFailure) throw error;
|
|
627
811
|
},
|
|
628
812
|
)
|
|
629
813
|
.finally(() => {
|
|
@@ -678,11 +862,16 @@ export class MCPConnectionPool {
|
|
|
678
862
|
try {
|
|
679
863
|
await connection.transport.close();
|
|
680
864
|
} catch (closeError) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
865
|
+
throw new MCPConnectionCleanupFailure(
|
|
866
|
+
new AggregateError(
|
|
867
|
+
[
|
|
868
|
+
pending.cancellationReason ?? new Error(`MCP connection acquisition abandoned: ${name}`),
|
|
869
|
+
closeError,
|
|
870
|
+
],
|
|
871
|
+
`MCP cancelled acquisition cleanup failed: ${name}`,
|
|
872
|
+
),
|
|
873
|
+
connection.transport,
|
|
874
|
+
);
|
|
686
875
|
}
|
|
687
876
|
throw pending.cancellationReason ?? new Error(`MCP connection acquisition abandoned: ${name}`);
|
|
688
877
|
}
|
|
@@ -938,6 +1127,7 @@ export class MCPConnectionPool {
|
|
|
938
1127
|
this.#shutdownController.abort(reason);
|
|
939
1128
|
for (const [key, pending] of this.#pending) {
|
|
940
1129
|
if (!pending.cancelled) {
|
|
1130
|
+
this.retainCancelledAcquireCleanup(key, pending.entry?.name ?? "unknown", pending, reason);
|
|
941
1131
|
pending.cancelled = true;
|
|
942
1132
|
pending.cancellationReason = reason;
|
|
943
1133
|
pending.openAbortController.abort(reason);
|
|
@@ -951,6 +1141,17 @@ export class MCPConnectionPool {
|
|
|
951
1141
|
pending.waiters.clear();
|
|
952
1142
|
if (this.#pending.get(key) === pending) this.#pending.delete(key);
|
|
953
1143
|
}
|
|
1144
|
+
for (const cleanup of this.#retiredAcquisitionCleanups.values()) {
|
|
1145
|
+
const failure = this.getPendingAcquireCleanupFailure(cleanup.error);
|
|
1146
|
+
if (!failure) continue;
|
|
1147
|
+
void this.closePendingAcquireCleanup(cleanup.key, cleanup.generation).catch(error =>
|
|
1148
|
+
logger.error("MCP pool shutdown pending acquire cleanup failed", {
|
|
1149
|
+
serverName: cleanup.serverName,
|
|
1150
|
+
poolKey: cleanup.key,
|
|
1151
|
+
error,
|
|
1152
|
+
}),
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
954
1155
|
for (const lease of this.#allLeases) lease.invalidate(reason);
|
|
955
1156
|
this.#allLeases.clear();
|
|
956
1157
|
const entries = [...new Set([...this.#entries.values(), ...this.#retiredEntries])];
|