@gajae-code/coding-agent 0.16.1 → 0.16.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 +70 -0
- package/dist/types/cli/args.d.ts +2 -2
- 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/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/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- 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/runtime-skill-discovery.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- 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 +1 -0
- package/dist/types/modes/types.d.ts +1 -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/bus/chat-daemon-runtime.d.ts +12 -0
- 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/cli/rows.d.ts +7 -0
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +23 -0
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/setup/paseo/announce.d.ts +111 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/index.d.ts +8 -2
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/dist/types/utils/external-editor.d.ts +5 -0
- package/package.json +7 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/root-flags.ts +5 -3
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock.ts +181 -17
- package/src/config/model-preset-registry.ts +5 -3
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +57 -11
- 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/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/discovery/builtin.ts +13 -4
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +10 -2
- package/src/extensibility/gjc-plugins/compiler.ts +208 -58
- 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/runtime-adapters.ts +758 -16
- package/src/extensibility/runtime-skill-discovery.ts +77 -151
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +44 -16
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +94 -6
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +72 -34
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +29 -14
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/model-selector.ts +166 -33
- 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 +393 -21
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +148 -5
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +47 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/search.md +2 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +171 -14
- package/src/runtime-mcp/pool.test.ts +209 -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/acp/adapter.ts +1 -1
- package/src/sdk/broker/broker.ts +5 -4
- package/src/sdk/bus/chat-daemon-control.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
- package/src/sdk/bus/slack-daemon.ts +92 -2
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/cli/rows.ts +47 -2
- package/src/sdk/cli/session-cli.ts +47 -23
- package/src/sdk/host/events.ts +25 -2
- package/src/sdk/host/host.ts +4 -1
- package/src/sdk/host/session-runtime.test.ts +36 -25
- package/src/sdk/host/session-runtime.ts +279 -43
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +60 -37
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +73 -30
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +123 -38
- package/src/session/contribution-prep.ts +214 -21
- package/src/setup/paseo/announce.ts +450 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +2 -1
- package/src/task/index.ts +17 -4
- package/src/tools/bash.ts +50 -21
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/index.ts +8 -2
- package/src/tools/job.ts +8 -2
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/search.ts +116 -58
- package/src/tools/skill.ts +22 -2
- package/src/tools/subagent.ts +3 -1
- package/src/tools/tool-catalog.generated.ts +9 -2
- package/src/utils/external-editor.ts +16 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
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])];
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
JsonRpcRequest,
|
|
14
14
|
JsonRpcResponse,
|
|
15
15
|
MCPRequestOptions,
|
|
16
|
+
MCPStdioPreparedLaunch,
|
|
16
17
|
MCPStdioServerConfig,
|
|
17
18
|
MCPTransport,
|
|
18
19
|
} from "../../runtime-mcp/types";
|
|
@@ -56,6 +57,22 @@ function buildMinimalStdioEnv(explicit?: Record<string, string>): Record<string,
|
|
|
56
57
|
return { ...env, ...explicit };
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
function combinedOperationAndCleanupFailure(operationError: unknown, cleanupError: unknown): MCPExpectedFailure {
|
|
61
|
+
return new MCPExpectedFailure(
|
|
62
|
+
new AggregateError(
|
|
63
|
+
[operationError, cleanupError],
|
|
64
|
+
"MCP stdio operation failed and subprocess cleanup also failed",
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type StdioConnectAttempt = {
|
|
70
|
+
generation: number;
|
|
71
|
+
revoked: boolean;
|
|
72
|
+
settled: boolean;
|
|
73
|
+
completion: Promise<void>;
|
|
74
|
+
};
|
|
75
|
+
|
|
59
76
|
export class StdioTransport implements MCPTransport {
|
|
60
77
|
#process: OwnedProcess | null = null;
|
|
61
78
|
#pendingRequests = new Map<
|
|
@@ -69,6 +86,10 @@ export class StdioTransport implements MCPTransport {
|
|
|
69
86
|
#readLoop: Promise<void> | null = null;
|
|
70
87
|
#stderrLoop: Promise<void> | null = null;
|
|
71
88
|
#closePromise: Promise<void> | null = null;
|
|
89
|
+
#connectGeneration = 0;
|
|
90
|
+
#connectAttempt: StdioConnectAttempt | null = null;
|
|
91
|
+
/** Retained until the per-attempt cleanup resolves successfully. */
|
|
92
|
+
#afterProcessExit: (() => Promise<void>) | null = null;
|
|
72
93
|
|
|
73
94
|
onClose?: () => void;
|
|
74
95
|
onError?: (error: Error) => void;
|
|
@@ -85,33 +106,122 @@ export class StdioTransport implements MCPTransport {
|
|
|
85
106
|
return true;
|
|
86
107
|
}
|
|
87
108
|
|
|
109
|
+
#registerCleanup(attempt: StdioConnectAttempt, cleanup: () => Promise<void>): void {
|
|
110
|
+
if (this.#connectAttempt !== attempt) {
|
|
111
|
+
throw new Error("MCP stdio attempt registered cleanup after its ownership ended");
|
|
112
|
+
}
|
|
113
|
+
if (this.#afterProcessExit && this.#afterProcessExit !== cleanup) {
|
|
114
|
+
throw new Error("MCP stdio attempt registered conflicting cleanup owners");
|
|
115
|
+
}
|
|
116
|
+
this.#afterProcessExit = cleanup;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#assertConnectAttemptActive(attempt: StdioConnectAttempt): void {
|
|
120
|
+
if (attempt.revoked || this.#connectAttempt !== attempt || this.#connectGeneration !== attempt.generation) {
|
|
121
|
+
throw new Error("MCP stdio connection attempt was closed");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#revokeConnectAttempt(): StdioConnectAttempt | null {
|
|
126
|
+
const attempt = this.#connectAttempt;
|
|
127
|
+
if (attempt && !attempt.revoked) {
|
|
128
|
+
attempt.revoked = true;
|
|
129
|
+
this.#connectGeneration++;
|
|
130
|
+
}
|
|
131
|
+
return attempt;
|
|
132
|
+
}
|
|
133
|
+
|
|
88
134
|
/**
|
|
89
135
|
* Start the subprocess and begin reading.
|
|
90
136
|
*/
|
|
91
137
|
async connect(): Promise<void> {
|
|
92
138
|
if (this.#connected) return;
|
|
93
|
-
if (this.#closePromise
|
|
139
|
+
if (this.#closePromise) {
|
|
140
|
+
throw new MCPExpectedFailure(new Error("MCP stdio child teardown is incomplete"));
|
|
141
|
+
}
|
|
142
|
+
if (this.#connectAttempt) {
|
|
143
|
+
if (!this.#connectAttempt.settled) return this.#connectAttempt.completion;
|
|
144
|
+
if (this.#process || this.#afterProcessExit) {
|
|
145
|
+
throw new MCPExpectedFailure(new Error("MCP stdio child teardown is incomplete"));
|
|
146
|
+
}
|
|
147
|
+
this.#connectAttempt = null;
|
|
148
|
+
}
|
|
149
|
+
if (this.#process || this.#afterProcessExit) {
|
|
94
150
|
throw new MCPExpectedFailure(new Error("MCP stdio child teardown is incomplete"));
|
|
95
151
|
}
|
|
96
152
|
|
|
97
|
-
const
|
|
153
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
154
|
+
const attempt: StdioConnectAttempt = {
|
|
155
|
+
generation: ++this.#connectGeneration,
|
|
156
|
+
revoked: false,
|
|
157
|
+
settled: false,
|
|
158
|
+
completion: promise,
|
|
159
|
+
};
|
|
160
|
+
this.#connectAttempt = attempt;
|
|
161
|
+
void this.#finishConnect(attempt).then(
|
|
162
|
+
() => {
|
|
163
|
+
attempt.settled = true;
|
|
164
|
+
resolve();
|
|
165
|
+
},
|
|
166
|
+
error => {
|
|
167
|
+
attempt.settled = true;
|
|
168
|
+
reject(error);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
void promise.catch(() => {});
|
|
172
|
+
return promise;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async #finishConnect(attempt: StdioConnectAttempt): Promise<void> {
|
|
176
|
+
let launch: MCPStdioPreparedLaunch = {
|
|
177
|
+
command: this.config.command,
|
|
178
|
+
args: Object.freeze([...(this.config.args ?? [])]),
|
|
179
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
180
|
+
registerCleanup: cleanup => this.#registerCleanup(attempt, cleanup),
|
|
181
|
+
};
|
|
98
182
|
const env = this.config.noInheritEnv
|
|
99
183
|
? buildMinimalStdioEnv(this.config.env)
|
|
100
184
|
: {
|
|
101
185
|
...Bun.env,
|
|
102
186
|
...this.config.env,
|
|
103
187
|
};
|
|
104
|
-
const cwd = this.config.cwd ?? process.cwd();
|
|
105
|
-
|
|
106
188
|
try {
|
|
107
|
-
this.#
|
|
108
|
-
|
|
189
|
+
this.#assertConnectAttemptActive(attempt);
|
|
190
|
+
if (this.config.prepareSpawn) launch = await this.config.prepareSpawn(launch);
|
|
191
|
+
if (launch.afterProcessExit) this.#registerCleanup(attempt, launch.afterProcessExit);
|
|
192
|
+
else if (!this.#afterProcessExit && this.config.afterProcessExit) {
|
|
193
|
+
this.#registerCleanup(attempt, this.config.afterProcessExit);
|
|
194
|
+
}
|
|
195
|
+
this.#assertConnectAttemptActive(attempt);
|
|
196
|
+
await this.config.spawnGuard?.(launch);
|
|
197
|
+
this.#assertConnectAttemptActive(attempt);
|
|
198
|
+
await this.config.afterSpawnGuardForTest?.();
|
|
199
|
+
this.#assertConnectAttemptActive(attempt);
|
|
200
|
+
const process = spawnOwnedProcess([launch.command, ...launch.args], {
|
|
201
|
+
cwd: launch.cwd,
|
|
109
202
|
env,
|
|
110
203
|
stdin: "pipe",
|
|
111
204
|
gracefulMs: CLOSE_WAIT_MS,
|
|
112
|
-
name: `mcp-stdio:${
|
|
205
|
+
name: `mcp-stdio:${launch.command}`,
|
|
113
206
|
});
|
|
207
|
+
if (attempt.revoked) {
|
|
208
|
+
const teardown = await process.dispose();
|
|
209
|
+
await process.awaitExit({ timeoutMs: CLOSE_WAIT_MS }).catch(() => ({ exited: false, code: null }));
|
|
210
|
+
if (teardown.status !== "terminated") {
|
|
211
|
+
this.#process = process;
|
|
212
|
+
throw new MCPExpectedFailure(new Error(`stdio child teardown ${teardown.status}`));
|
|
213
|
+
}
|
|
214
|
+
throw new Error("MCP stdio connection attempt was closed");
|
|
215
|
+
}
|
|
216
|
+
this.#process = process;
|
|
114
217
|
} catch (error) {
|
|
218
|
+
if (!this.#process) {
|
|
219
|
+
try {
|
|
220
|
+
await this.#runAfterProcessExit();
|
|
221
|
+
} catch (cleanupError) {
|
|
222
|
+
throw combinedOperationAndCleanupFailure(error, cleanupError);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
115
225
|
throw new MCPExpectedFailure(error);
|
|
116
226
|
}
|
|
117
227
|
|
|
@@ -367,19 +477,32 @@ export class StdioTransport implements MCPTransport {
|
|
|
367
477
|
}
|
|
368
478
|
}
|
|
369
479
|
|
|
480
|
+
async #runAfterProcessExit(): Promise<void> {
|
|
481
|
+
const cleanup = this.#afterProcessExit;
|
|
482
|
+
if (!cleanup) return;
|
|
483
|
+
await cleanup();
|
|
484
|
+
if (this.#afterProcessExit === cleanup) this.#afterProcessExit = null;
|
|
485
|
+
}
|
|
486
|
+
|
|
370
487
|
async close(): Promise<void> {
|
|
371
488
|
await this.#closeInternal(false);
|
|
372
489
|
}
|
|
373
490
|
|
|
374
491
|
#closeInternal(fromReadLoop: boolean, failure?: MCPExpectedFailure): Promise<void> {
|
|
375
492
|
if (this.#closePromise) return this.#closePromise;
|
|
376
|
-
|
|
493
|
+
const connectAttempt = this.#revokeConnectAttempt();
|
|
494
|
+
this.#closePromise = this.#finishClose(fromReadLoop, failure, connectAttempt).finally(() => {
|
|
495
|
+
if (this.#connectAttempt === connectAttempt) this.#connectAttempt = null;
|
|
377
496
|
this.#closePromise = null;
|
|
378
497
|
});
|
|
379
498
|
return this.#closePromise;
|
|
380
499
|
}
|
|
381
500
|
|
|
382
|
-
async #finishClose(
|
|
501
|
+
async #finishClose(
|
|
502
|
+
fromReadLoop: boolean,
|
|
503
|
+
failure: MCPExpectedFailure | undefined,
|
|
504
|
+
connectAttempt: StdioConnectAttempt | null,
|
|
505
|
+
): Promise<void> {
|
|
383
506
|
const wasConnected = this.#connected;
|
|
384
507
|
this.#connected = false;
|
|
385
508
|
|
|
@@ -388,16 +511,36 @@ export class StdioTransport implements MCPTransport {
|
|
|
388
511
|
}
|
|
389
512
|
this.#pendingRequests.clear();
|
|
390
513
|
|
|
514
|
+
let primaryFailure: unknown = failure;
|
|
515
|
+
if (connectAttempt) {
|
|
516
|
+
try {
|
|
517
|
+
await connectAttempt.completion;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (primaryFailure === undefined && this.#process) primaryFailure = error;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
391
522
|
const stdin = this.#getStdin();
|
|
392
523
|
const process = this.#process;
|
|
393
524
|
if (process) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
525
|
+
try {
|
|
526
|
+
stdin?.end();
|
|
527
|
+
const teardown = await process.dispose();
|
|
528
|
+
await process.awaitExit({ timeoutMs: CLOSE_WAIT_MS }).catch(() => ({ exited: false, code: null }));
|
|
529
|
+
if (teardown.status !== "terminated") {
|
|
530
|
+
throw new MCPExpectedFailure(new Error(`stdio child teardown ${teardown.status}`));
|
|
531
|
+
}
|
|
532
|
+
this.#process = null;
|
|
533
|
+
} catch (cleanupError) {
|
|
534
|
+
if (primaryFailure) throw combinedOperationAndCleanupFailure(primaryFailure, cleanupError);
|
|
535
|
+
throw cleanupError;
|
|
399
536
|
}
|
|
400
|
-
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
await this.#runAfterProcessExit();
|
|
541
|
+
} catch (cleanupError) {
|
|
542
|
+
if (primaryFailure) throw combinedOperationAndCleanupFailure(primaryFailure, cleanupError);
|
|
543
|
+
throw cleanupError;
|
|
401
544
|
}
|
|
402
545
|
|
|
403
546
|
if (!fromReadLoop && this.#readLoop) {
|
|
@@ -413,12 +556,3 @@ export class StdioTransport implements MCPTransport {
|
|
|
413
556
|
if (wasConnected) this.onClose?.();
|
|
414
557
|
}
|
|
415
558
|
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Create and connect a stdio transport.
|
|
419
|
-
*/
|
|
420
|
-
export async function createStdioTransport(config: MCPStdioServerConfig): Promise<StdioTransport> {
|
|
421
|
-
const transport = new StdioTransport(config);
|
|
422
|
-
await transport.connect();
|
|
423
|
-
return transport;
|
|
424
|
-
}
|
package/src/runtime-mcp/types.ts
CHANGED
|
@@ -95,6 +95,22 @@ export interface MCPServerConfigBase {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** Stdio server configuration */
|
|
98
|
+
export interface MCPStdioSpawnLaunch {
|
|
99
|
+
command: string;
|
|
100
|
+
args: readonly string[];
|
|
101
|
+
cwd: string;
|
|
102
|
+
/** Transport-supplied registrar for retryable cleanup before preparation allocates resources. */
|
|
103
|
+
registerCleanup?: (cleanup: () => Promise<void>) => void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface MCPStdioPreparedLaunch extends MCPStdioSpawnLaunch {
|
|
107
|
+
/**
|
|
108
|
+
* Per-attempt cleanup run after confirmed child exit or failed spawn. The same
|
|
109
|
+
* callback may already have been registered during preparation.
|
|
110
|
+
*/
|
|
111
|
+
afterProcessExit?: () => Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
export interface MCPStdioServerConfig extends MCPServerConfigBase {
|
|
99
115
|
type?: "stdio"; // Default if not specified
|
|
100
116
|
command: string;
|
|
@@ -108,6 +124,14 @@ export interface MCPStdioServerConfig extends MCPServerConfigBase {
|
|
|
108
124
|
*/
|
|
109
125
|
noInheritEnv?: boolean;
|
|
110
126
|
cwd?: string;
|
|
127
|
+
/** Internal trusted-boundary preparation of immutable per-spawn authority. */
|
|
128
|
+
prepareSpawn?: (launch: MCPStdioSpawnLaunch) => Promise<MCPStdioPreparedLaunch>;
|
|
129
|
+
/** Internal trusted-boundary check run immediately before process creation. */
|
|
130
|
+
spawnGuard?: (launch: MCPStdioSpawnLaunch) => Promise<void>;
|
|
131
|
+
/** Internal cleanup run only after the owned stdio child is confirmed exited. */
|
|
132
|
+
afterProcessExit?: () => Promise<void>;
|
|
133
|
+
/** Test seam for deterministic mutation after the guard and before process creation. */
|
|
134
|
+
afterSpawnGuardForTest?: () => Promise<void>;
|
|
111
135
|
}
|
|
112
136
|
|
|
113
137
|
/** HTTP server configuration (Streamable HTTP transport) */
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -875,7 +875,7 @@ export class AcpSdkAdapter {
|
|
|
875
875
|
|
|
876
876
|
#expiredOwnedLeaseFrame(frame: Record<string, unknown>): boolean {
|
|
877
877
|
const response = this.#leaseErrorFrame(frame);
|
|
878
|
-
if (
|
|
878
|
+
if (response?.ok !== false) return false;
|
|
879
879
|
const leaseId = typeof response.leaseId === "string" ? response.leaseId : "";
|
|
880
880
|
const connectionId = typeof response.connectionId === "string" ? response.connectionId : "";
|
|
881
881
|
if (!leaseId || !this.#ownsLeaseId(leaseId)) return false;
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -3095,11 +3095,12 @@ export class Broker {
|
|
|
3095
3095
|
}
|
|
3096
3096
|
const limit = stored?.limit ?? requestedLimit ?? SESSION_LIST_DEFAULT_LIMIT;
|
|
3097
3097
|
const observedAt = stored?.observedAt ?? (scope === undefined ? undefined : new Date().toISOString());
|
|
3098
|
+
const resolveSessionId = typeof input.resolveSessionId === "string" ? input.resolveSessionId : undefined;
|
|
3098
3099
|
const snapshot = stored ?? {
|
|
3099
|
-
sessions:
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3100
|
+
sessions: (scope === undefined
|
|
3101
|
+
? [...result.sessions]
|
|
3102
|
+
: result.sessions.filter(session => scopeMatchesLocator(scope, session.locator))
|
|
3103
|
+
).filter(session => resolveSessionId === undefined || session.sessionId === resolveSessionId),
|
|
3103
3104
|
indexSeq: result.indexSeq,
|
|
3104
3105
|
warnings: [...result.warnings],
|
|
3105
3106
|
limit,
|