@gajae-code/coding-agent 0.17.4 → 0.17.5
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 +73 -0
- package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
- package/dist/types/config/file-lock.d.ts +4 -2
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
- package/dist/types/harness-control-plane/finalize.d.ts +14 -3
- package/dist/types/main.d.ts +1 -1
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +13 -6
- package/dist/types/sdk/broker/broker.d.ts +4 -2
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/identity.d.ts +2 -0
- package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
- package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
- package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +7 -2
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tools/bash-interactive.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +10 -1
- package/dist/types/tools/subagent.d.ts +8 -1
- package/package.json +7 -7
- package/src/cli/public-command-registry.ts +8 -2
- package/src/cli/setup-cli.ts +1 -0
- package/src/cli/stats-cli.ts +11 -1
- package/src/config/autorouting-tier-map.ts +529 -3
- package/src/config/file-lock.ts +59 -14
- package/src/config/model-discovery-manager.ts +4 -4
- package/src/config/model-profile-activation.ts +83 -23
- package/src/config/model-registry.ts +72 -12
- package/src/config/model-resolver.ts +6 -1
- package/src/coordinator-mcp/question-state.ts +4 -1
- package/src/coordinator-mcp/server.ts +14 -6
- package/src/exec/bash-executor.ts +3 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
- package/src/gjc-runtime/state-writer.ts +317 -6
- package/src/harness-control-plane/control-endpoint.ts +24 -6
- package/src/harness-control-plane/finalize.ts +312 -25
- package/src/harness-control-plane/owner.ts +323 -89
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/main.ts +69 -19
- package/src/modes/acp/acp-agent.ts +581 -37
- package/src/modes/components/gjc-bundle-settings.ts +21 -1
- package/src/modes/components/tool-status-header.ts +72 -6
- package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
- package/src/runtime-mcp/loader.ts +6 -3
- package/src/runtime-mcp/manager.ts +177 -75
- package/src/sdk/broker/broker.ts +1057 -150
- package/src/sdk/broker/ensure.ts +21 -3
- package/src/sdk/broker/identity.ts +13 -0
- package/src/sdk/broker/managed-task-dag.ts +1927 -0
- package/src/sdk/broker/managed-task-verification.ts +642 -0
- package/src/sdk/broker/spawn-authority.ts +140 -7
- package/src/sdk/broker/startup-failure.ts +80 -1
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +22 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
- package/src/sdk/cli/session-cli.ts +18 -0
- package/src/sdk/host/session-runtime.test.ts +46 -0
- package/src/sdk/host/session-runtime.ts +13 -6
- package/src/sdk/lifecycle/service.ts +2 -0
- package/src/sdk/prompt-deadline-flush.ts +26 -4
- package/src/sdk/session.ts +587 -115
- package/src/session/agent-session.ts +40 -12
- package/src/session/internal/managed-session-scope.ts +10 -0
- package/src/session/internal/managed-session-storage.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/executor.ts +46 -17
- package/src/task/types.ts +2 -0
- package/src/tools/bash-interactive.ts +2 -0
- package/src/tools/bash.ts +20 -0
- package/src/tools/fetch.ts +21 -5
- package/src/tools/subagent-render.ts +24 -1
- package/src/tools/subagent.ts +27 -1
- package/src/utils/mupdf.ts +11 -4
package/src/sdk/session.ts
CHANGED
|
@@ -67,9 +67,11 @@ import {
|
|
|
67
67
|
formatModelString,
|
|
68
68
|
parseModelPattern,
|
|
69
69
|
parseModelString,
|
|
70
|
+
refreshMissingQualifiedModelProviders,
|
|
70
71
|
resolveAllowedModels,
|
|
71
72
|
resolveModelChainWithAuth,
|
|
72
73
|
resolveModelRoleValue,
|
|
74
|
+
resolveStartupModelRefreshSelectors,
|
|
73
75
|
type ScopedModelSelection,
|
|
74
76
|
} from "../config/model-resolver";
|
|
75
77
|
import { normalizeModelSelectorValue } from "../config/model-selector-value";
|
|
@@ -116,9 +118,11 @@ import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injec
|
|
|
116
118
|
import { currentActivationFingerprint } from "../extensibility/gjc-plugins/lifecycle";
|
|
117
119
|
import {
|
|
118
120
|
buildPluginMcpConfigs,
|
|
121
|
+
type GjcPluginMcpServerProvenance,
|
|
119
122
|
getGjcPluginToolDeclarations,
|
|
120
123
|
loadAlwaysOnPluginTools,
|
|
121
124
|
renderAlwaysOnSystemAppendices,
|
|
125
|
+
safePluginMcpDiagnostic,
|
|
122
126
|
} from "../extensibility/gjc-plugins/runtime-adapters";
|
|
123
127
|
import {
|
|
124
128
|
GjcRuntimeFindingAccumulator,
|
|
@@ -145,8 +149,16 @@ import {
|
|
|
145
149
|
createOptionalRuntimeServices,
|
|
146
150
|
type OptionalRuntimeServicesOverrides,
|
|
147
151
|
} from "../runtime/optional-runtime-services";
|
|
148
|
-
import {
|
|
152
|
+
import {
|
|
153
|
+
DeferredMCPTool,
|
|
154
|
+
isMCPStartupTimeoutError,
|
|
155
|
+
loadAllMCPConfigs,
|
|
156
|
+
MCPManager,
|
|
157
|
+
type MCPToolCache,
|
|
158
|
+
resolveMCPToolCache,
|
|
159
|
+
} from "../runtime-mcp";
|
|
149
160
|
import type { MCPLoadResult } from "../runtime-mcp/manager";
|
|
161
|
+
import { MCP_STARTUP_WAIT_GRACE_MS } from "../runtime-mcp/startup-policy";
|
|
150
162
|
import type { MCPServerConfig } from "../runtime-mcp/types";
|
|
151
163
|
import {
|
|
152
164
|
getNotificationConfig,
|
|
@@ -609,9 +621,9 @@ export interface CreateAgentSessionOptions {
|
|
|
609
621
|
inheritedAsyncJobManager?: AsyncJobManager;
|
|
610
622
|
/**
|
|
611
623
|
* W6b: the parent's scope-held MCP facade, handed to a canonical sub-session so
|
|
612
|
-
* it can inherit
|
|
613
|
-
*
|
|
614
|
-
*
|
|
624
|
+
* it can inherit MCP tools without owning the manager. Replaces the removed
|
|
625
|
+
* `MCPManager.instance()` inheritance path; the sub-session subscribes to catalog
|
|
626
|
+
* changes but never connects or disposes this manager.
|
|
615
627
|
*/
|
|
616
628
|
inheritedMcpManager?: import("../runtime-mcp/manager").MCPManager;
|
|
617
629
|
|
|
@@ -1248,6 +1260,29 @@ const DEFERRED_MCP_CONFIG_STARTUP_ERROR = "MCP tools could not be loaded.";
|
|
|
1248
1260
|
const MAX_EXACT_MCP_TOOL_NAME_LENGTH = 100;
|
|
1249
1261
|
const pluginMcpManagerServers = new WeakMap<MCPManager, ReadonlySet<string>>();
|
|
1250
1262
|
const conventionalMcpManagerServers = new WeakMap<MCPManager, ReadonlySet<string>>();
|
|
1263
|
+
const failedPluginMcpManagerServers = new WeakMap<MCPManager, ReadonlySet<string>>();
|
|
1264
|
+
const pluginMcpCleanupFailures = new WeakSet<MCPManager>();
|
|
1265
|
+
|
|
1266
|
+
function sameMcpToolSnapshot(first: readonly CustomTool[], second: readonly CustomTool[]): boolean {
|
|
1267
|
+
return first.length === second.length && first.every((tool, index) => tool === second[index]);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function classifyInheritedMcpToolNames(
|
|
1271
|
+
manager: MCPManager,
|
|
1272
|
+
tools: readonly CustomTool[],
|
|
1273
|
+
): { pluginMcpToolNames: string[]; conventionalMcpToolNames: string[] } {
|
|
1274
|
+
const pluginServers = pluginMcpManagerServers.get(manager);
|
|
1275
|
+
const conventionalServers = conventionalMcpManagerServers.get(manager);
|
|
1276
|
+
const pluginMcpToolNames: string[] = [];
|
|
1277
|
+
const conventionalMcpToolNames: string[] = [];
|
|
1278
|
+
for (const tool of tools) {
|
|
1279
|
+
const serverName = tool.mcpServerName;
|
|
1280
|
+
if (serverName === undefined) continue;
|
|
1281
|
+
if (pluginServers?.has(serverName)) pluginMcpToolNames.push(tool.name);
|
|
1282
|
+
else if (conventionalServers?.has(serverName)) conventionalMcpToolNames.push(tool.name);
|
|
1283
|
+
}
|
|
1284
|
+
return { pluginMcpToolNames, conventionalMcpToolNames };
|
|
1285
|
+
}
|
|
1251
1286
|
|
|
1252
1287
|
class ExactMcpToolNameCollisionError extends Error {
|
|
1253
1288
|
constructor(toolNames: Iterable<string>) {
|
|
@@ -1262,23 +1297,43 @@ class ExactMcpToolNameCollisionError extends Error {
|
|
|
1262
1297
|
class McpManagerCleanupError extends Error {
|
|
1263
1298
|
readonly code = "MCP_MANAGER_CLEANUP_FAILED";
|
|
1264
1299
|
constructor(cause: unknown) {
|
|
1265
|
-
super(`Owned MCP manager cleanup failed: ${
|
|
1300
|
+
super(`Owned MCP manager cleanup failed: ${safePluginMcpDiagnostic(cause)}`, { cause });
|
|
1266
1301
|
this.name = "McpManagerCleanupError";
|
|
1267
1302
|
}
|
|
1268
1303
|
}
|
|
1269
1304
|
|
|
1305
|
+
class McpStartupCleanupRetryError extends Error {
|
|
1306
|
+
readonly primaryError: unknown;
|
|
1307
|
+
constructor(primaryError: unknown) {
|
|
1308
|
+
super("Owned MCP manager cleanup failed during startup", { cause: primaryError });
|
|
1309
|
+
this.name = "McpStartupCleanupRetryError";
|
|
1310
|
+
this.primaryError = primaryError;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1270
1314
|
class McpManagerCleanupDiagnosticError extends Error {
|
|
1271
1315
|
readonly code = "MCP_MANAGER_CLEANUP_FAILED";
|
|
1272
1316
|
readonly primaryError: unknown;
|
|
1273
1317
|
readonly cleanupDiagnostic: { code: "MCP_MANAGER_CLEANUP_FAILED"; cause: unknown };
|
|
1274
1318
|
constructor(primaryError: unknown, cleanupError: unknown) {
|
|
1275
|
-
super(
|
|
1319
|
+
super(safePluginMcpDiagnostic(primaryError), { cause: primaryError });
|
|
1276
1320
|
this.name = "McpManagerCleanupDiagnosticError";
|
|
1277
1321
|
this.primaryError = primaryError;
|
|
1278
1322
|
this.cleanupDiagnostic = { code: "MCP_MANAGER_CLEANUP_FAILED", cause: cleanupError };
|
|
1279
1323
|
}
|
|
1280
1324
|
}
|
|
1281
1325
|
|
|
1326
|
+
class McpPluginStartupCleanupDiagnosticError extends Error {
|
|
1327
|
+
readonly primaryError: unknown;
|
|
1328
|
+
readonly cleanupDiagnostic: { code: "MCP_MANAGER_CLEANUP_FAILED"; cause: unknown };
|
|
1329
|
+
constructor(primaryError: unknown, cleanupError: unknown) {
|
|
1330
|
+
super("GJC plugin MCP startup and cleanup failed");
|
|
1331
|
+
this.name = "McpPluginStartupCleanupDiagnosticError";
|
|
1332
|
+
this.primaryError = primaryError;
|
|
1333
|
+
this.cleanupDiagnostic = { code: "MCP_MANAGER_CLEANUP_FAILED", cause: cleanupError };
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1282
1337
|
function safeIsInstanceOf<T extends object>(value: unknown, ctor: abstract new (...args: any[]) => T): boolean {
|
|
1283
1338
|
try {
|
|
1284
1339
|
return value instanceof ctor;
|
|
@@ -1301,8 +1356,8 @@ function safeCleanupDiagnosticForLog(value: unknown): { code: string; cause: str
|
|
|
1301
1356
|
const code = safeReadProperty(value, "code");
|
|
1302
1357
|
const nestedCause = safeReadProperty(value, "cause");
|
|
1303
1358
|
return {
|
|
1304
|
-
code: typeof code === "string" ? code : "MCP_MANAGER_CLEANUP_FAILED",
|
|
1305
|
-
cause:
|
|
1359
|
+
code: typeof code === "string" ? safePluginMcpDiagnostic(code) : "MCP_MANAGER_CLEANUP_FAILED",
|
|
1360
|
+
cause: safePluginMcpDiagnostic(nestedCause === undefined ? value : nestedCause),
|
|
1306
1361
|
};
|
|
1307
1362
|
}
|
|
1308
1363
|
function safeReadCleanupDiagnostic(value: unknown): unknown {
|
|
@@ -1520,7 +1575,33 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1520
1575
|
let asyncJobManagerOwned = false;
|
|
1521
1576
|
let asyncJobManagerAdmitted = false;
|
|
1522
1577
|
let priorAsyncJobManager: AsyncJobManager | undefined;
|
|
1523
|
-
|
|
1578
|
+
type OwnedMcpManagerCleanup = () => Promise<void>;
|
|
1579
|
+
let cleanupOwnedMcpManager: OwnedMcpManagerCleanup | undefined;
|
|
1580
|
+
let cleanupOwnedMcpManagerOwner: MCPManager | undefined;
|
|
1581
|
+
const registerOwnedMcpManagerCleanup = (owned: MCPManager): OwnedMcpManagerCleanup => {
|
|
1582
|
+
let cleanupAttempt: Promise<void> | undefined;
|
|
1583
|
+
const cleanup: OwnedMcpManagerCleanup = () => {
|
|
1584
|
+
if (cleanupAttempt) return cleanupAttempt;
|
|
1585
|
+
cleanupAttempt = Promise.resolve()
|
|
1586
|
+
.then(() => owned.disconnectAll())
|
|
1587
|
+
.then(
|
|
1588
|
+
() => {
|
|
1589
|
+
if (cleanupOwnedMcpManager === cleanup) {
|
|
1590
|
+
cleanupOwnedMcpManager = undefined;
|
|
1591
|
+
cleanupOwnedMcpManagerOwner = undefined;
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
error => {
|
|
1595
|
+
cleanupAttempt = undefined;
|
|
1596
|
+
throw error;
|
|
1597
|
+
},
|
|
1598
|
+
);
|
|
1599
|
+
return cleanupAttempt;
|
|
1600
|
+
};
|
|
1601
|
+
cleanupOwnedMcpManager = cleanup;
|
|
1602
|
+
cleanupOwnedMcpManagerOwner = owned;
|
|
1603
|
+
return cleanup;
|
|
1604
|
+
};
|
|
1524
1605
|
const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
|
|
1525
1606
|
const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
|
|
1526
1607
|
const resolvedAgentDisplayName = options.agentDisplayName ?? (isCanonicalSubSession ? "sub" : "main");
|
|
@@ -1558,6 +1639,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1558
1639
|
unsubscribeCredentialDisabled = undefined;
|
|
1559
1640
|
unsubscribe?.();
|
|
1560
1641
|
};
|
|
1642
|
+
let inheritedMcpToolsPublisher: ((tools: readonly CustomTool[]) => void) | undefined;
|
|
1643
|
+
let inheritedMcpToolsUnsubscribe: (() => void) | undefined;
|
|
1644
|
+
const stopInheritedMcpToolsSubscription = (): void => {
|
|
1645
|
+
inheritedMcpToolsPublisher = undefined;
|
|
1646
|
+
const unsubscribe = inheritedMcpToolsUnsubscribe;
|
|
1647
|
+
inheritedMcpToolsUnsubscribe = undefined;
|
|
1648
|
+
unsubscribe?.();
|
|
1649
|
+
};
|
|
1561
1650
|
|
|
1562
1651
|
try {
|
|
1563
1652
|
// Subscribe before any getApiKey() call so startup model probes can't fire a
|
|
@@ -1904,6 +1993,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1904
1993
|
);
|
|
1905
1994
|
}
|
|
1906
1995
|
if (!options.modelRegistry && !attemptedStartupCacheAdmission) {
|
|
1996
|
+
// Match the CLI's awaited startup discovery: the background refresh below
|
|
1997
|
+
// is not a readiness barrier, so resolving a fresh configured default first
|
|
1998
|
+
// can reject a model that the provider would discover moments later.
|
|
1999
|
+
const startupModelSelectors = resolveStartupModelRefreshSelectors(
|
|
2000
|
+
{
|
|
2001
|
+
model: options.modelPattern ?? (options.model ? formatModelString(options.model) : undefined),
|
|
2002
|
+
credential: options.credentialSelector ? "session" : undefined,
|
|
2003
|
+
resume: hasExistingSession,
|
|
2004
|
+
hasStartupProfile: Boolean(options.activeModelProfile || settings.get("modelProfile.default")),
|
|
2005
|
+
},
|
|
2006
|
+
settings,
|
|
2007
|
+
);
|
|
2008
|
+
await refreshMissingQualifiedModelProviders(startupModelSelectors, modelRegistry, credentialSessionId);
|
|
1907
2009
|
modelRegistry.refreshInBackground("online-if-uncached", credentialSessionId);
|
|
1908
2010
|
}
|
|
1909
2011
|
|
|
@@ -2357,11 +2459,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2357
2459
|
};
|
|
2358
2460
|
let mcpManager: MCPManager | undefined = options.mcpManager;
|
|
2359
2461
|
let ownsMcpManager = false;
|
|
2462
|
+
let ownedMcpToolCache: MCPToolCache | null | undefined;
|
|
2463
|
+
const getOwnedMcpToolCache = async (): Promise<MCPToolCache | null> => {
|
|
2464
|
+
if (ownedMcpToolCache === undefined)
|
|
2465
|
+
ownedMcpToolCache = await resolveMCPToolCache(undefined, getAgentDbPath(agentDir));
|
|
2466
|
+
return ownedMcpToolCache;
|
|
2467
|
+
};
|
|
2360
2468
|
const cwdCapturingToolNames: string[] = [];
|
|
2361
2469
|
const ownedConventionalMcpServerNames = new Set<string>();
|
|
2362
|
-
|
|
2363
|
-
let
|
|
2364
|
-
let
|
|
2470
|
+
const cachedConventionalMcpServerNames = new Set<string>();
|
|
2471
|
+
let ownedMcpManagerToolNames: string[] = [];
|
|
2472
|
+
let ownedMcpManagerTools: readonly CustomTool[] = [];
|
|
2473
|
+
let publishOwnedMcpTools = false;
|
|
2365
2474
|
const notificationDebounceTimers = new Map<string, Timer>();
|
|
2366
2475
|
const wireMcpManagerCallbacks = (manager: MCPManager): void => {
|
|
2367
2476
|
manager.setOnPromptsChanged(serverName => {
|
|
@@ -2416,6 +2525,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2416
2525
|
if (previousManager) await previousManager.disconnectAll().catch(() => {});
|
|
2417
2526
|
pluginMcpToolNames.length = 0;
|
|
2418
2527
|
conventionalMcpToolNames.length = 0;
|
|
2528
|
+
ownedConventionalMcpServerNames.clear();
|
|
2529
|
+
cachedConventionalMcpServerNames.clear();
|
|
2530
|
+
ownedMcpManagerToolNames = [];
|
|
2531
|
+
ownedMcpManagerTools = [];
|
|
2532
|
+
publishOwnedMcpTools = false;
|
|
2419
2533
|
let nextManager: MCPManager | undefined;
|
|
2420
2534
|
try {
|
|
2421
2535
|
const loaded = await loadAllMCPConfigs(to, {
|
|
@@ -2439,11 +2553,51 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2439
2553
|
]),
|
|
2440
2554
|
),
|
|
2441
2555
|
};
|
|
2556
|
+
const conventionalCacheServerNames = new Set(
|
|
2557
|
+
Object.keys(loaded.configs).filter(name => !pluginNames.has(name)),
|
|
2558
|
+
);
|
|
2559
|
+
for (const name of conventionalCacheServerNames) ownedConventionalMcpServerNames.add(name);
|
|
2560
|
+
publishOwnedMcpTools = Object.keys(mergedConfigs).length > 0;
|
|
2442
2561
|
if (Object.keys(mergedConfigs).length > 0) {
|
|
2443
|
-
nextManager = new MCPManager(
|
|
2562
|
+
nextManager = new MCPManager(
|
|
2563
|
+
to,
|
|
2564
|
+
conventionalCacheServerNames.size > 0 ? await getOwnedMcpToolCache() : null,
|
|
2565
|
+
{
|
|
2566
|
+
sharedPoolIdleMs: settings.get("mcp.sharedPoolIdleMs"),
|
|
2567
|
+
toolCacheServerNames: conventionalCacheServerNames,
|
|
2568
|
+
},
|
|
2569
|
+
);
|
|
2444
2570
|
nextManager.setAuthStorage(authStorage);
|
|
2445
2571
|
wireMcpManagerCallbacks(nextManager);
|
|
2446
2572
|
const result = await nextManager.connectServers(mergedConfigs, mergedSources as never);
|
|
2573
|
+
const connectedPluginNames = new Set(result.connectedServers.filter(name => pluginNames.has(name)));
|
|
2574
|
+
pluginMcpManagerServers.set(nextManager, new Set(pluginNames));
|
|
2575
|
+
conventionalMcpManagerServers.set(nextManager, conventionalCacheServerNames);
|
|
2576
|
+
failedPluginMcpManagerServers.set(nextManager, new Set());
|
|
2577
|
+
for (const tool of result.tools) {
|
|
2578
|
+
const serverName = tool.mcpServerName;
|
|
2579
|
+
if (
|
|
2580
|
+
tool instanceof DeferredMCPTool &&
|
|
2581
|
+
serverName &&
|
|
2582
|
+
conventionalCacheServerNames.has(serverName)
|
|
2583
|
+
) {
|
|
2584
|
+
cachedConventionalMcpServerNames.add(serverName);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
ownedMcpManagerTools = result.tools as CustomTool[];
|
|
2588
|
+
ownedMcpManagerToolNames = result.tools.map(tool => tool.name);
|
|
2589
|
+
const unsettledConventionalNames = [...conventionalCacheServerNames].filter(
|
|
2590
|
+
name =>
|
|
2591
|
+
nextManager?.getConnectionStatus(name) === "connecting" ||
|
|
2592
|
+
cachedConventionalMcpServerNames.has(name),
|
|
2593
|
+
);
|
|
2594
|
+
if (pluginNames.size > 0) {
|
|
2595
|
+
if (connectedPluginNames.size > 0 && unsettledConventionalNames.length === 0) {
|
|
2596
|
+
nextManager.sealConnectionSet();
|
|
2597
|
+
} else {
|
|
2598
|
+
nextManager.freezeConnectionSet();
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2447
2601
|
nextCustomTools.push(...(result.tools as CustomTool[]));
|
|
2448
2602
|
nextCwdCapturing.push(...result.tools.map(tool => tool.name));
|
|
2449
2603
|
for (const tool of result.tools) {
|
|
@@ -2461,14 +2615,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2461
2615
|
mcpManager = nextManager;
|
|
2462
2616
|
ownsMcpManager = Boolean(nextManager);
|
|
2463
2617
|
await session.replaceOwnedMcpManager(nextManager);
|
|
2464
|
-
await session.refreshMCPTools((nextManager?.getTools() ?? []) as CustomTool[]
|
|
2618
|
+
await session.refreshMCPTools((nextManager?.getTools() ?? []) as CustomTool[], {
|
|
2619
|
+
mandatoryMCPToolNames: pluginMcpToolNames,
|
|
2620
|
+
});
|
|
2465
2621
|
}
|
|
2466
2622
|
cwdCapturingToolNames.length = 0;
|
|
2467
2623
|
cwdCapturingToolNames.push(...nextCwdCapturing);
|
|
2468
2624
|
await session.replaceNamedCustomTools(
|
|
2469
2625
|
previousCwdCapturing.filter(name => !nextCustomTools.some(tool => tool.name === name)),
|
|
2470
2626
|
nextCustomTools,
|
|
2627
|
+
{ mandatoryMCPToolNames: pluginMcpToolNames },
|
|
2471
2628
|
);
|
|
2629
|
+
wireOwnedMcpToolSync();
|
|
2472
2630
|
};
|
|
2473
2631
|
|
|
2474
2632
|
/**
|
|
@@ -3031,10 +3189,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3031
3189
|
const exactMcpToolNames: string[] = [];
|
|
3032
3190
|
const pluginMcpToolNames: string[] = [];
|
|
3033
3191
|
const conventionalMcpToolNames: string[] = [];
|
|
3192
|
+
let inheritedMcpManager: MCPManager | undefined;
|
|
3193
|
+
let inheritedMcpManagerToolNames: string[] = [];
|
|
3194
|
+
let inheritedMcpToolsSnapshot: readonly CustomTool[] = [];
|
|
3195
|
+
let pendingInheritedMcpToolsSnapshot: readonly CustomTool[] | undefined;
|
|
3034
3196
|
let deferredExactMcpConfig: { manager: MCPManager; configPath: string } | undefined;
|
|
3035
3197
|
let deferredConventionalMcp:
|
|
3036
3198
|
| {
|
|
3037
3199
|
manager: MCPManager;
|
|
3200
|
+
cleanup: OwnedMcpManagerCleanup;
|
|
3038
3201
|
configs: Record<string, MCPServerConfig>;
|
|
3039
3202
|
sources: Record<string, SourceMeta>;
|
|
3040
3203
|
conventionalConfigs: Record<string, MCPServerConfig>;
|
|
@@ -3154,76 +3317,179 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3154
3317
|
mergedSources: Record<string, SourceMeta>,
|
|
3155
3318
|
conventionalConfigs: Record<string, MCPServerConfig>,
|
|
3156
3319
|
pluginNames: ReadonlySet<string>,
|
|
3320
|
+
pluginMcpProvenance: ReadonlyMap<string, GjcPluginMcpServerProvenance>,
|
|
3321
|
+
cleanupOwned: OwnedMcpManagerCleanup,
|
|
3157
3322
|
): Promise<MCPLoadResult> => {
|
|
3158
3323
|
let result: MCPLoadResult;
|
|
3159
3324
|
try {
|
|
3160
3325
|
result = await owned.connectServers(mergedConfigs, mergedSources as never);
|
|
3161
3326
|
} catch (error) {
|
|
3162
|
-
if (
|
|
3327
|
+
if (
|
|
3328
|
+
safeIsInstanceOf(error, McpManagerCleanupError) ||
|
|
3329
|
+
safeIsInstanceOf(error, McpManagerCleanupDiagnosticError) ||
|
|
3330
|
+
safeReadCleanupDiagnostic(error) !== undefined
|
|
3331
|
+
)
|
|
3332
|
+
throw error;
|
|
3163
3333
|
// Avoid leaking partially-started server processes on failure.
|
|
3334
|
+
const retryAfterStartupCleanupFailure = options.enableMCP === false && !options.deferMcpConfigStartup;
|
|
3164
3335
|
let cleanupError: unknown;
|
|
3336
|
+
let cleanupFailed = false;
|
|
3165
3337
|
try {
|
|
3166
|
-
await
|
|
3338
|
+
await cleanupOwned();
|
|
3167
3339
|
} catch (disconnectError) {
|
|
3340
|
+
cleanupFailed = true;
|
|
3168
3341
|
cleanupError = disconnectError;
|
|
3169
|
-
} finally {
|
|
3170
|
-
cleanupOwnedMcpManager = undefined;
|
|
3171
3342
|
}
|
|
3172
|
-
if (
|
|
3343
|
+
if (cleanupFailed) {
|
|
3344
|
+
if (retryAfterStartupCleanupFailure) throw new McpStartupCleanupRetryError(error);
|
|
3345
|
+
if (pluginNames.size > 0) {
|
|
3346
|
+
throw new McpPluginStartupCleanupDiagnosticError(error, cleanupError);
|
|
3347
|
+
}
|
|
3348
|
+
throw attachMcpCleanupDiagnostic(error, cleanupError);
|
|
3349
|
+
}
|
|
3173
3350
|
throw error;
|
|
3174
3351
|
}
|
|
3352
|
+
const conventionalServerNames = new Set(
|
|
3353
|
+
Object.keys(conventionalConfigs).filter(name => !pluginNames.has(name)),
|
|
3354
|
+
);
|
|
3355
|
+
for (const tool of result.tools) {
|
|
3356
|
+
if (tool instanceof DeferredMCPTool && conventionalServerNames.has(tool.mcpServerName)) {
|
|
3357
|
+
cachedConventionalMcpServerNames.add(tool.mcpServerName);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3175
3360
|
for (const [server, err] of result.errors) {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3361
|
+
const provenance = pluginMcpProvenance.get(server);
|
|
3362
|
+
if (provenance) {
|
|
3363
|
+
gjcFindings.add({
|
|
3364
|
+
identity: provenance.identity,
|
|
3365
|
+
surfaceId: provenance.surfaceId,
|
|
3366
|
+
code: "runtime_mismatch",
|
|
3367
|
+
message: `MCP server connection failed: ${safePluginMcpDiagnostic(err)}`,
|
|
3368
|
+
decision: "error",
|
|
3369
|
+
provenance: {
|
|
3370
|
+
source: "plugin-bundle",
|
|
3371
|
+
plugin: provenance.identity.name,
|
|
3372
|
+
scope: provenance.identity.scope,
|
|
3373
|
+
},
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
if (provenance) {
|
|
3377
|
+
logger.error("GJC plugin MCP connection failed", {
|
|
3378
|
+
path: `mcp:${server}`,
|
|
3379
|
+
error: safePluginMcpDiagnostic(err),
|
|
3380
|
+
});
|
|
3381
|
+
} else if (!isMCPStartupTimeoutError(err)) {
|
|
3382
|
+
logger.warn("MCP server connection failed", {
|
|
3184
3383
|
path: `mcp:${server}`,
|
|
3185
|
-
|
|
3384
|
+
// MCPLoadResult stores failures as strings, so their original
|
|
3385
|
+
// type is unavailable here; use the manager classifier's safe fallback.
|
|
3386
|
+
error: "transport-error",
|
|
3186
3387
|
});
|
|
3187
3388
|
}
|
|
3188
3389
|
}
|
|
3189
|
-
const
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
// "connected" inside the microtask between connectServers() resolving
|
|
3193
|
-
// and this synchronous check must not be torn down either.
|
|
3194
|
-
const unsettledConventionalNames = Object.keys(conventionalConfigs).filter(
|
|
3195
|
-
name => owned.getConnectionStatus(name) !== "disconnected",
|
|
3390
|
+
const failedPluginNames = new Set([...result.errors.keys()].filter(name => pluginMcpProvenance.has(name)));
|
|
3391
|
+
const connectedPluginNames = new Set(
|
|
3392
|
+
result.connectedServers.filter(name => pluginNames.has(name) && !failedPluginNames.has(name)),
|
|
3196
3393
|
);
|
|
3197
|
-
const
|
|
3394
|
+
const trustedPluginServerNames = new Set(
|
|
3395
|
+
[...pluginMcpProvenance.keys()].filter(name => !failedPluginNames.has(name)),
|
|
3396
|
+
);
|
|
3397
|
+
const connectedTrustedPluginNames = new Set(
|
|
3398
|
+
[...connectedPluginNames].filter(name => trustedPluginServerNames.has(name)),
|
|
3399
|
+
);
|
|
3400
|
+
const unsettledPluginNames = [...pluginNames].filter(
|
|
3401
|
+
name =>
|
|
3402
|
+
!failedPluginNames.has(name) &&
|
|
3403
|
+
!connectedPluginNames.has(name) &&
|
|
3404
|
+
owned.getConnectionStatus(name) !== "disconnected",
|
|
3405
|
+
);
|
|
3406
|
+
let pluginCleanupFailed = false;
|
|
3407
|
+
for (const server of unsettledPluginNames) {
|
|
3408
|
+
const provenance = pluginMcpProvenance.get(server);
|
|
3409
|
+
if (!provenance) continue;
|
|
3410
|
+
let message =
|
|
3411
|
+
"MCP server did not produce a complete startup result within its declared startup window; its tools are unavailable for this session";
|
|
3412
|
+
let cleanupDiagnostic: string | undefined;
|
|
3413
|
+
failedPluginNames.add(server);
|
|
3414
|
+
try {
|
|
3415
|
+
await owned.disconnectServer(server);
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
pluginCleanupFailed = true;
|
|
3418
|
+
cleanupDiagnostic = safePluginMcpDiagnostic(error);
|
|
3419
|
+
}
|
|
3420
|
+
if (owned.getConnectionStatus(server) !== "disconnected") {
|
|
3421
|
+
pluginCleanupFailed = true;
|
|
3422
|
+
cleanupDiagnostic ??= "server remained connected or connecting after disconnect";
|
|
3423
|
+
}
|
|
3424
|
+
if (cleanupDiagnostic !== undefined) message += `; cleanup diagnostic: ${cleanupDiagnostic}`;
|
|
3425
|
+
gjcFindings.add({
|
|
3426
|
+
identity: provenance.identity,
|
|
3427
|
+
surfaceId: provenance.surfaceId,
|
|
3428
|
+
code: "runtime_mismatch",
|
|
3429
|
+
message,
|
|
3430
|
+
decision: "error",
|
|
3431
|
+
provenance: {
|
|
3432
|
+
source: "plugin-bundle",
|
|
3433
|
+
plugin: provenance.identity.name,
|
|
3434
|
+
scope: provenance.identity.scope,
|
|
3435
|
+
},
|
|
3436
|
+
});
|
|
3437
|
+
logger.error("GJC plugin MCP startup did not settle", {
|
|
3438
|
+
path: `mcp:${server}`,
|
|
3439
|
+
error: message,
|
|
3440
|
+
...(cleanupDiagnostic === undefined ? {} : { cleanupDiagnostic }),
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
failedPluginMcpManagerServers.set(owned, new Set(failedPluginNames));
|
|
3444
|
+
if (pluginCleanupFailed) pluginMcpCleanupFailures.add(owned);
|
|
3445
|
+
const successfulTools = result.tools.filter(
|
|
3446
|
+
tool => tool.mcpServerName === undefined || !failedPluginNames.has(tool.mcpServerName),
|
|
3447
|
+
);
|
|
3448
|
+
// Only a connection still in its startup window or a cached fallback
|
|
3449
|
+
// needs the mixed manager to remain mutable. Successfully connected
|
|
3450
|
+
// ordinary tools do not delay the synchronous seal; cached fallbacks keep
|
|
3451
|
+
// their reconnect path available while they remain published.
|
|
3452
|
+
const unsettledConventionalNames = [...conventionalServerNames].filter(
|
|
3453
|
+
name => owned.getConnectionStatus(name) === "connecting" || cachedConventionalMcpServerNames.has(name),
|
|
3454
|
+
);
|
|
3455
|
+
const retainOwnedManager =
|
|
3456
|
+
result.connectedServers.length > 0 ||
|
|
3457
|
+
unsettledConventionalNames.length > 0 ||
|
|
3458
|
+
successfulTools.length > 0 ||
|
|
3459
|
+
pluginCleanupFailed;
|
|
3198
3460
|
if (retainOwnedManager) {
|
|
3199
3461
|
mcpManager = owned;
|
|
3200
3462
|
ownsMcpManager = true;
|
|
3201
|
-
customTools.push(...(
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3463
|
+
customTools.push(...(successfulTools as CustomTool[]));
|
|
3464
|
+
ownedMcpManagerTools = successfulTools as CustomTool[];
|
|
3465
|
+
ownedMcpManagerToolNames = successfulTools.map(tool => tool.name);
|
|
3466
|
+
cwdCapturingToolNames.push(...successfulTools.map(tool => tool.name));
|
|
3467
|
+
for (const name of conventionalServerNames) ownedConventionalMcpServerNames.add(name);
|
|
3468
|
+
pluginMcpManagerServers.set(owned, trustedPluginServerNames);
|
|
3469
|
+
conventionalMcpManagerServers.set(owned, conventionalServerNames);
|
|
3470
|
+
for (const tool of successfulTools) {
|
|
3207
3471
|
const serverName = tool.mcpServerName;
|
|
3208
3472
|
if (serverName === undefined) continue;
|
|
3209
|
-
if (
|
|
3473
|
+
if (connectedTrustedPluginNames.has(serverName)) pluginMcpToolNames.push(tool.name);
|
|
3210
3474
|
else {
|
|
3211
3475
|
conventionalMcpToolNames.push(tool.name);
|
|
3212
|
-
ownedConventionalMcpToolNames.push(tool.name);
|
|
3213
3476
|
}
|
|
3214
3477
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
//
|
|
3218
|
-
//
|
|
3219
|
-
|
|
3220
|
-
|
|
3478
|
+
publishOwnedMcpTools = true;
|
|
3479
|
+
// Keep plugin config authority fixed even while a conventional cached
|
|
3480
|
+
// fallback needs reconnects; a full reload would disconnect and lose the
|
|
3481
|
+
// plugin-bundle configs because native discovery does not reload them.
|
|
3482
|
+
if (pluginNames.size > 0) {
|
|
3483
|
+
if (connectedTrustedPluginNames.size > 0 && unsettledConventionalNames.length === 0) {
|
|
3484
|
+
owned.sealConnectionSet();
|
|
3485
|
+
} else {
|
|
3486
|
+
owned.freezeConnectionSet();
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3221
3489
|
} else {
|
|
3222
3490
|
try {
|
|
3223
|
-
await
|
|
3224
|
-
cleanupOwnedMcpManager = undefined;
|
|
3491
|
+
await cleanupOwned();
|
|
3225
3492
|
} catch (cleanupError) {
|
|
3226
|
-
cleanupOwnedMcpManager = undefined;
|
|
3227
3493
|
throw new McpManagerCleanupError(cleanupError);
|
|
3228
3494
|
}
|
|
3229
3495
|
}
|
|
@@ -3240,7 +3506,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3240
3506
|
owned.setAuthStorage(authStorage);
|
|
3241
3507
|
mcpManager = owned;
|
|
3242
3508
|
ownsMcpManager = true;
|
|
3243
|
-
|
|
3509
|
+
registerOwnedMcpManagerCleanup(owned);
|
|
3244
3510
|
if (options.deferMcpConfigStartup) {
|
|
3245
3511
|
deferredExactMcpConfig = { manager: owned, configPath: explicitMcpConfigPath };
|
|
3246
3512
|
} else {
|
|
@@ -3308,7 +3574,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3308
3574
|
// tools are surfaced as always-on tools rather than gated behind MCP
|
|
3309
3575
|
// selection.
|
|
3310
3576
|
try {
|
|
3311
|
-
const { configs, quarantine } = await buildPluginMcpConfigs({ cwd });
|
|
3577
|
+
const { configs, quarantine, serverProvenance: pluginMcpProvenance } = await buildPluginMcpConfigs({ cwd });
|
|
3312
3578
|
for (const q of quarantine) {
|
|
3313
3579
|
gjcFindings.add({
|
|
3314
3580
|
identity: q.identity,
|
|
@@ -3335,9 +3601,30 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3335
3601
|
// own bypass of the same mismatch at the manager boundary).
|
|
3336
3602
|
const mergedSourceMetas = mergedSources as Record<string, SourceMeta>;
|
|
3337
3603
|
if (Object.keys(mergedConfigs).length > 0) {
|
|
3338
|
-
const
|
|
3604
|
+
const conventionalCacheServerNames = new Set(
|
|
3605
|
+
Object.keys(conventionalConfigs).filter(name => !pluginNames.has(name)),
|
|
3606
|
+
);
|
|
3607
|
+
const maxPluginMcpTimeoutMs = Object.values(configs).reduce((maximum, config) => {
|
|
3608
|
+
const timeout = config.timeout;
|
|
3609
|
+
return typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
|
|
3610
|
+
? Math.max(maximum, timeout)
|
|
3611
|
+
: maximum;
|
|
3612
|
+
}, 0);
|
|
3613
|
+
const maxPluginMcpStartupWaitMs =
|
|
3614
|
+
maxPluginMcpTimeoutMs > 0 ? maxPluginMcpTimeoutMs + MCP_STARTUP_WAIT_GRACE_MS : undefined;
|
|
3615
|
+
const owned = new MCPManager(
|
|
3616
|
+
cwd,
|
|
3617
|
+
conventionalCacheServerNames.size > 0 ? await getOwnedMcpToolCache() : null,
|
|
3618
|
+
{
|
|
3619
|
+
sharedPoolIdleMs: settings.get("mcp.sharedPoolIdleMs"),
|
|
3620
|
+
toolCacheServerNames: conventionalCacheServerNames,
|
|
3621
|
+
...(maxPluginMcpStartupWaitMs === undefined
|
|
3622
|
+
? {}
|
|
3623
|
+
: { maxStartupTimeoutMs: maxPluginMcpStartupWaitMs }),
|
|
3624
|
+
},
|
|
3625
|
+
);
|
|
3339
3626
|
owned.setAuthStorage(authStorage);
|
|
3340
|
-
|
|
3627
|
+
const cleanupOwned = registerOwnedMcpManagerCleanup(owned);
|
|
3341
3628
|
// Interactive launches may paint before MCP connects; the deferred
|
|
3342
3629
|
// starter reconnects this manager after first paint and a startup
|
|
3343
3630
|
// turn barrier keeps the first prompt from racing tool registration.
|
|
@@ -3347,6 +3634,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3347
3634
|
if (options.deferMcpConfigStartup && pluginNames.size === 0) {
|
|
3348
3635
|
deferredConventionalMcp = {
|
|
3349
3636
|
manager: owned,
|
|
3637
|
+
cleanup: cleanupOwned,
|
|
3350
3638
|
configs: mergedConfigs,
|
|
3351
3639
|
sources: mergedSourceMetas,
|
|
3352
3640
|
conventionalConfigs,
|
|
@@ -3363,37 +3651,60 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3363
3651
|
mergedSourceMetas,
|
|
3364
3652
|
conventionalConfigs,
|
|
3365
3653
|
pluginNames,
|
|
3654
|
+
pluginMcpProvenance,
|
|
3655
|
+
cleanupOwned,
|
|
3366
3656
|
);
|
|
3367
3657
|
}
|
|
3368
3658
|
}
|
|
3369
3659
|
} catch (error) {
|
|
3370
|
-
if (safeIsInstanceOf(error,
|
|
3660
|
+
if (safeIsInstanceOf(error, McpStartupCleanupRetryError)) {
|
|
3661
|
+
throw (error as McpStartupCleanupRetryError).primaryError;
|
|
3662
|
+
}
|
|
3663
|
+
const pluginStartupCleanupFailure = safeIsInstanceOf(error, McpPluginStartupCleanupDiagnosticError)
|
|
3664
|
+
? (error as McpPluginStartupCleanupDiagnosticError)
|
|
3665
|
+
: undefined;
|
|
3666
|
+
if (
|
|
3667
|
+
!pluginStartupCleanupFailure &&
|
|
3668
|
+
(safeIsInstanceOf(error, McpManagerCleanupError) ||
|
|
3669
|
+
safeIsInstanceOf(error, McpManagerCleanupDiagnosticError) ||
|
|
3670
|
+
safeReadCleanupDiagnostic(error) !== undefined)
|
|
3671
|
+
)
|
|
3672
|
+
throw error;
|
|
3371
3673
|
gjcProducersComplete = false;
|
|
3372
|
-
const cleanupDiagnostic =
|
|
3674
|
+
const cleanupDiagnostic =
|
|
3675
|
+
pluginStartupCleanupFailure?.cleanupDiagnostic ?? safeReadCleanupDiagnostic(error);
|
|
3373
3676
|
logger.warn("Failed to wire GJC plugin MCP servers", {
|
|
3374
|
-
error:
|
|
3677
|
+
error: pluginStartupCleanupFailure
|
|
3678
|
+
? safePluginMcpDiagnostic(pluginStartupCleanupFailure.primaryError)
|
|
3679
|
+
: safeErrorForLog(error),
|
|
3375
3680
|
cleanupDiagnostic: safeCleanupDiagnosticForLog(cleanupDiagnostic),
|
|
3376
3681
|
});
|
|
3377
3682
|
}
|
|
3378
3683
|
} else if (isCanonicalSubSession) {
|
|
3379
|
-
// Subagents inherit the parent's
|
|
3380
|
-
//
|
|
3381
|
-
//
|
|
3382
|
-
// scope; process-global state is never consulted for MCP routing.
|
|
3684
|
+
// Subagents inherit the parent's MCP tools without owning the manager.
|
|
3685
|
+
// They subscribe to catalog changes but never connect or dispose it; the
|
|
3686
|
+
// facade is carried explicitly by the parent scope.
|
|
3383
3687
|
const inherited = mcpManager ?? options.inheritedMcpManager;
|
|
3384
3688
|
if (inherited) {
|
|
3385
3689
|
try {
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3690
|
+
inheritedMcpManager = inherited;
|
|
3691
|
+
inheritedMcpToolsUnsubscribe = inherited.subscribeToToolsChanged(tools => {
|
|
3692
|
+
const snapshot = tools.map(tool => tool as CustomTool);
|
|
3693
|
+
if (inheritedMcpToolsPublisher) inheritedMcpToolsPublisher(snapshot);
|
|
3694
|
+
else pendingInheritedMcpToolsSnapshot = snapshot;
|
|
3695
|
+
});
|
|
3696
|
+
const inheritedTools = inherited.getTools() as CustomTool[];
|
|
3697
|
+
inheritedMcpToolsSnapshot = inheritedTools;
|
|
3698
|
+
inheritedMcpManagerToolNames = inheritedTools.map(tool => tool.name);
|
|
3699
|
+
customTools.push(...inheritedTools);
|
|
3700
|
+
const classifiedTools = classifyInheritedMcpToolNames(inherited, inheritedTools);
|
|
3701
|
+
pluginMcpToolNames.push(...classifiedTools.pluginMcpToolNames);
|
|
3702
|
+
conventionalMcpToolNames.push(...classifiedTools.conventionalMcpToolNames);
|
|
3396
3703
|
} catch (error) {
|
|
3704
|
+
stopInheritedMcpToolsSubscription();
|
|
3705
|
+
inheritedMcpManager = undefined;
|
|
3706
|
+
inheritedMcpToolsSnapshot = [];
|
|
3707
|
+
pendingInheritedMcpToolsSnapshot = undefined;
|
|
3397
3708
|
logger.warn("Failed to inherit MCP tools in subagent", { error: safeErrorForLog(error) });
|
|
3398
3709
|
}
|
|
3399
3710
|
}
|
|
@@ -4960,6 +5271,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
4960
5271
|
// carried by the host replay ring through the internal runtime seam above.
|
|
4961
5272
|
if (autoroutingInactive) session.configWarnings.push(AUTOROUTING_INACTIVE_WARNING);
|
|
4962
5273
|
hasSession = true;
|
|
5274
|
+
const cleanupOwnedManager = cleanupOwnedMcpManager;
|
|
5275
|
+
const sessionOwnedMcpManager = ownsMcpManager ? mcpManager : undefined;
|
|
5276
|
+
if (cleanupOwnedManager && cleanupOwnedMcpManagerOwner !== sessionOwnedMcpManager) {
|
|
5277
|
+
session.registerToolSessionCleanup(cleanupOwnedManager);
|
|
5278
|
+
}
|
|
4963
5279
|
if (masterModeContext) {
|
|
4964
5280
|
// One scoped, no-probe peer snapshot immediately before the FIRST accepted
|
|
4965
5281
|
// provider request; see createMasterPeerSnapshotContributor for the
|
|
@@ -5141,59 +5457,166 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
5141
5457
|
// createAgentSession rather than surfacing as an unhandled rejection.
|
|
5142
5458
|
if (!options.deferMemoryBackendStartup) await startMemoryBackend();
|
|
5143
5459
|
|
|
5144
|
-
// Wire
|
|
5145
|
-
//
|
|
5146
|
-
//
|
|
5147
|
-
//
|
|
5148
|
-
const
|
|
5149
|
-
|
|
5150
|
-
|
|
5460
|
+
// Wire late catalog publications from every owned manager into the live
|
|
5461
|
+
// session. Plugin-only managers also change after startup through
|
|
5462
|
+
// tools/list_changed; mixed managers additionally publish cached fallbacks.
|
|
5463
|
+
// Shared by eager startup and the deferred starter.
|
|
5464
|
+
const wireOwnedMcpToolSync = (initialTools?: readonly CustomTool[]): void => {
|
|
5465
|
+
const manager = mcpManager;
|
|
5466
|
+
if (!manager || !publishOwnedMcpTools || manager.isToolsOnly()) return;
|
|
5467
|
+
// Late MCP connections can publish near-simultaneously.
|
|
5151
5468
|
// Serialize the swaps so an older snapshot cannot interleave with a
|
|
5152
5469
|
// newer one inside replaceNamedCustomTools and leave a stale list.
|
|
5153
5470
|
// Each link swallows (and logs) its own failure so one bad
|
|
5154
5471
|
// publication cannot kill the chain for every later one.
|
|
5155
|
-
let
|
|
5156
|
-
|
|
5157
|
-
|
|
5472
|
+
let ownedMcpToolsSync: Promise<void> = Promise.resolve();
|
|
5473
|
+
let acceptingOwnedMcpTools = true;
|
|
5474
|
+
const syncOwnedMcpTools = (tools: readonly CustomTool[]): Promise<void> => {
|
|
5475
|
+
if (!acceptingOwnedMcpTools) return ownedMcpToolsSync;
|
|
5476
|
+
ownedMcpToolsSync = ownedMcpToolsSync
|
|
5158
5477
|
.then(async () => {
|
|
5159
|
-
if (session.isDisposed) return;
|
|
5160
|
-
const
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5478
|
+
if (session.isDisposed || mcpManager !== manager) return;
|
|
5479
|
+
const pluginServerNames = new Set(pluginMcpManagerServers.get(manager));
|
|
5480
|
+
const conventionalServerNames = new Set(conventionalMcpManagerServers.get(manager));
|
|
5481
|
+
// Plugin authority is fixed from validated bundle provenance when
|
|
5482
|
+
// this manager is created. SourceMeta can be caller-supplied later.
|
|
5483
|
+
for (const serverName of manager.getAllServerNames()) {
|
|
5484
|
+
if (!pluginServerNames.has(serverName)) conventionalServerNames.add(serverName);
|
|
5485
|
+
}
|
|
5486
|
+
for (const tool of tools) {
|
|
5487
|
+
const serverName = tool.mcpServerName;
|
|
5488
|
+
if (!serverName) continue;
|
|
5489
|
+
if (!pluginServerNames.has(serverName)) conventionalServerNames.add(serverName);
|
|
5490
|
+
}
|
|
5491
|
+
conventionalMcpManagerServers.set(manager, conventionalServerNames);
|
|
5492
|
+
const failedPluginServers = failedPluginMcpManagerServers.get(manager);
|
|
5493
|
+
const nextCachedConventionalMcpServerNames = new Set(cachedConventionalMcpServerNames);
|
|
5494
|
+
for (const serverName of nextCachedConventionalMcpServerNames) {
|
|
5495
|
+
if (!tools.some(tool => tool.mcpServerName === serverName)) {
|
|
5496
|
+
nextCachedConventionalMcpServerNames.delete(serverName);
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
for (const tool of tools) {
|
|
5500
|
+
if (
|
|
5501
|
+
tool instanceof DeferredMCPTool &&
|
|
5502
|
+
tool.mcpServerName &&
|
|
5503
|
+
conventionalServerNames.has(tool.mcpServerName)
|
|
5504
|
+
) {
|
|
5505
|
+
nextCachedConventionalMcpServerNames.add(tool.mcpServerName);
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
const nextTools = tools.filter(tool => {
|
|
5509
|
+
const serverName = tool.mcpServerName;
|
|
5510
|
+
if (!serverName) return false;
|
|
5511
|
+
if (
|
|
5512
|
+
failedPluginServers?.has(serverName) &&
|
|
5513
|
+
manager.getConnectionStatus(serverName) !== "connected"
|
|
5514
|
+
) {
|
|
5515
|
+
return false;
|
|
5516
|
+
}
|
|
5517
|
+
return pluginServerNames.has(serverName) || conventionalServerNames.has(serverName);
|
|
5518
|
+
});
|
|
5519
|
+
const previousNames = ownedMcpManagerToolNames;
|
|
5520
|
+
const nextToolNames = nextTools.map(tool => tool.name);
|
|
5165
5521
|
const previousSet = new Set(previousNames);
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5522
|
+
const nextPluginToolNames = nextTools
|
|
5523
|
+
.filter(tool => tool.mcpServerName && pluginServerNames.has(tool.mcpServerName))
|
|
5524
|
+
.map(tool => tool.name);
|
|
5525
|
+
const nextConventionalToolNames = nextTools
|
|
5526
|
+
.filter(tool => tool.mcpServerName && conventionalServerNames.has(tool.mcpServerName))
|
|
5527
|
+
.map(tool => tool.name);
|
|
5528
|
+
const nextMandatoryMcpToolNames = [
|
|
5529
|
+
...pluginMcpToolNames.filter(name => !previousSet.has(name)),
|
|
5530
|
+
...nextPluginToolNames,
|
|
5531
|
+
];
|
|
5532
|
+
const nextConventionalMcpToolNames = [
|
|
5533
|
+
...conventionalMcpToolNames.filter(name => !previousSet.has(name)),
|
|
5534
|
+
...nextConventionalToolNames,
|
|
5535
|
+
];
|
|
5536
|
+
const nextCwdCapturingToolNames = [
|
|
5169
5537
|
...cwdCapturingToolNames.filter(name => !previousSet.has(name)),
|
|
5170
|
-
...
|
|
5538
|
+
...nextToolNames,
|
|
5539
|
+
];
|
|
5540
|
+
const nextOwnedPluginServersConnected = [...pluginServerNames].some(
|
|
5541
|
+
name => manager.getConnectionStatus(name) === "connected",
|
|
5171
5542
|
);
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5543
|
+
const nextOwnedConventionalMcpServerNames = new Set(conventionalServerNames);
|
|
5544
|
+
const hasPublishedCachedTool = nextTools.some(
|
|
5545
|
+
tool =>
|
|
5546
|
+
tool.mcpServerName !== undefined &&
|
|
5547
|
+
nextCachedConventionalMcpServerNames.has(tool.mcpServerName),
|
|
5548
|
+
);
|
|
5549
|
+
try {
|
|
5550
|
+
await session.replaceNamedCustomTools(previousNames, nextTools, {
|
|
5551
|
+
mandatoryMCPToolNames: nextMandatoryMcpToolNames,
|
|
5552
|
+
activateNewTools: false,
|
|
5553
|
+
});
|
|
5554
|
+
} catch (error) {
|
|
5555
|
+
ownedMcpManagerToolNames = [...new Set([...previousNames, ...nextToolNames])];
|
|
5556
|
+
throw error;
|
|
5557
|
+
}
|
|
5558
|
+
ownedMcpManagerToolNames = nextToolNames;
|
|
5559
|
+
ownedMcpManagerTools = nextTools;
|
|
5560
|
+
pluginMcpToolNames.splice(0, pluginMcpToolNames.length, ...nextMandatoryMcpToolNames);
|
|
5561
|
+
conventionalMcpToolNames.splice(0, conventionalMcpToolNames.length, ...nextConventionalMcpToolNames);
|
|
5562
|
+
cwdCapturingToolNames.splice(0, cwdCapturingToolNames.length, ...nextCwdCapturingToolNames);
|
|
5563
|
+
ownedConventionalMcpServerNames.clear();
|
|
5564
|
+
for (const name of nextOwnedConventionalMcpServerNames) ownedConventionalMcpServerNames.add(name);
|
|
5565
|
+
cachedConventionalMcpServerNames.clear();
|
|
5566
|
+
for (const name of nextCachedConventionalMcpServerNames) cachedConventionalMcpServerNames.add(name);
|
|
5567
|
+
// Mixed plugin + conventional sessions defer the seal while a
|
|
5568
|
+
// conventional server is connecting. Cached fallback servers retain
|
|
5569
|
+
// their reconnect path while any of their tools remain published,
|
|
5570
|
+
// including after reconnect replaces DeferredMCPTool with MCPTool.
|
|
5571
|
+
// Derive that hold from this snapshot so removing/replacing the tools
|
|
5572
|
+
// restores the fixed-connection plugin contract.
|
|
5177
5573
|
if (
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
!
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5574
|
+
nextOwnedPluginServersConnected &&
|
|
5575
|
+
!manager.isConnectionSetSealed() &&
|
|
5576
|
+
![...nextOwnedConventionalMcpServerNames].some(
|
|
5577
|
+
name => manager.getConnectionStatus(name) === "connecting",
|
|
5578
|
+
) &&
|
|
5579
|
+
!hasPublishedCachedTool
|
|
5184
5580
|
) {
|
|
5185
|
-
|
|
5581
|
+
manager.sealConnectionSet();
|
|
5186
5582
|
}
|
|
5187
5583
|
})
|
|
5188
5584
|
.catch(error => {
|
|
5189
|
-
logger.warn("Failed to publish
|
|
5585
|
+
logger.warn("Failed to publish owned MCP tools", { error: safeErrorForLog(error) });
|
|
5190
5586
|
});
|
|
5191
|
-
return
|
|
5587
|
+
return ownedMcpToolsSync;
|
|
5192
5588
|
};
|
|
5193
|
-
|
|
5194
|
-
|
|
5589
|
+
session.registerToolSessionCleanup(async () => {
|
|
5590
|
+
acceptingOwnedMcpTools = false;
|
|
5591
|
+
await ownedMcpToolsSync;
|
|
5592
|
+
});
|
|
5593
|
+
manager.setOnToolsChanged(tools => {
|
|
5594
|
+
void syncOwnedMcpTools(tools);
|
|
5195
5595
|
});
|
|
5196
|
-
|
|
5596
|
+
const catalog = manager.getToolCatalogSnapshot();
|
|
5597
|
+
if (initialTools !== undefined) {
|
|
5598
|
+
// Deferred startup already registered its captured result. Re-read
|
|
5599
|
+
// after that await so a publication during registration is not lost.
|
|
5600
|
+
const currentTools =
|
|
5601
|
+
catalog.publication === "published" ? catalog.tools.map(tool => tool as CustomTool) : initialTools;
|
|
5602
|
+
void syncOwnedMcpTools(currentTools);
|
|
5603
|
+
} else if (catalog.publication === "published") {
|
|
5604
|
+
const failedPluginServers = failedPluginMcpManagerServers.get(manager);
|
|
5605
|
+
const catalogTools = catalog.tools
|
|
5606
|
+
.filter(tool => {
|
|
5607
|
+
const serverName = tool.mcpServerName;
|
|
5608
|
+
return (
|
|
5609
|
+
serverName !== undefined &&
|
|
5610
|
+
(!failedPluginServers?.has(serverName) || manager.getConnectionStatus(serverName) === "connected")
|
|
5611
|
+
);
|
|
5612
|
+
})
|
|
5613
|
+
.map(tool => tool as CustomTool);
|
|
5614
|
+
if (!sameMcpToolSnapshot(catalogTools, ownedMcpManagerTools)) {
|
|
5615
|
+
void syncOwnedMcpTools(catalog.tools.map(tool => tool as CustomTool));
|
|
5616
|
+
}
|
|
5617
|
+
} else if (!pluginMcpCleanupFailures.has(manager)) {
|
|
5618
|
+
void syncOwnedMcpTools(ownedMcpManagerTools);
|
|
5619
|
+
}
|
|
5197
5620
|
};
|
|
5198
5621
|
const wireOwnedMcpManagerLifecycle = (): void => {
|
|
5199
5622
|
if (!mcpManager) return;
|
|
@@ -5204,20 +5627,66 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
5204
5627
|
postmortem.register("mcp-notification-cleanup", clearDebounceTimers);
|
|
5205
5628
|
wireMcpManagerCallbacks(mcpManager);
|
|
5206
5629
|
};
|
|
5630
|
+
const wireInheritedMcpToolSync = async (): Promise<void> => {
|
|
5631
|
+
const manager = inheritedMcpManager;
|
|
5632
|
+
if (!manager) return;
|
|
5633
|
+
let inheritedMcpToolsSync: Promise<void> = Promise.resolve();
|
|
5634
|
+
const syncInheritedTools = (tools: readonly CustomTool[]): Promise<void> => {
|
|
5635
|
+
const snapshot = [...tools];
|
|
5636
|
+
inheritedMcpToolsSync = inheritedMcpToolsSync
|
|
5637
|
+
.then(async () => {
|
|
5638
|
+
if (session.isDisposed) return;
|
|
5639
|
+
const previousNames = inheritedMcpManagerToolNames;
|
|
5640
|
+
const nextNames = snapshot.map(tool => tool.name);
|
|
5641
|
+
const classifiedTools = classifyInheritedMcpToolNames(manager, snapshot);
|
|
5642
|
+
try {
|
|
5643
|
+
await session.replaceNamedCustomTools(previousNames, snapshot, {
|
|
5644
|
+
mandatoryMCPToolNames: classifiedTools.pluginMcpToolNames,
|
|
5645
|
+
activateNewTools: false,
|
|
5646
|
+
});
|
|
5647
|
+
} catch (error) {
|
|
5648
|
+
inheritedMcpManagerToolNames = [...new Set([...previousNames, ...nextNames])];
|
|
5649
|
+
throw error;
|
|
5650
|
+
}
|
|
5651
|
+
inheritedMcpManagerToolNames = nextNames;
|
|
5652
|
+
pluginMcpToolNames.splice(0, pluginMcpToolNames.length, ...classifiedTools.pluginMcpToolNames);
|
|
5653
|
+
conventionalMcpToolNames.splice(
|
|
5654
|
+
0,
|
|
5655
|
+
conventionalMcpToolNames.length,
|
|
5656
|
+
...classifiedTools.conventionalMcpToolNames,
|
|
5657
|
+
);
|
|
5658
|
+
})
|
|
5659
|
+
.catch(error => {
|
|
5660
|
+
logger.warn("Failed to publish inherited MCP tools", { error: safeErrorForLog(error) });
|
|
5661
|
+
});
|
|
5662
|
+
return inheritedMcpToolsSync;
|
|
5663
|
+
};
|
|
5664
|
+
inheritedMcpToolsPublisher = tools => {
|
|
5665
|
+
void syncInheritedTools(tools);
|
|
5666
|
+
};
|
|
5667
|
+
session.registerToolSessionCleanup(async () => {
|
|
5668
|
+
stopInheritedMcpToolsSubscription();
|
|
5669
|
+
await inheritedMcpToolsSync;
|
|
5670
|
+
});
|
|
5671
|
+
const initialSnapshot = pendingInheritedMcpToolsSnapshot ?? inheritedMcpToolsSnapshot;
|
|
5672
|
+
pendingInheritedMcpToolsSnapshot = undefined;
|
|
5673
|
+
await syncInheritedTools(initialSnapshot);
|
|
5674
|
+
};
|
|
5207
5675
|
// Exact-config managers do not receive reactive callbacks; their tools are
|
|
5208
5676
|
// registered once in the session-owned catalog. A pending conventional
|
|
5209
5677
|
// deferral wires its own callbacks from the starter after it connects, so
|
|
5210
5678
|
// nothing is wired here yet.
|
|
5211
5679
|
if (mcpManager && !options.mcpManager && explicitMcpConfigPath === undefined && !deferredConventionalMcp) {
|
|
5212
|
-
if (
|
|
5213
|
-
|
|
5680
|
+
if (publishOwnedMcpTools) {
|
|
5681
|
+
wireOwnedMcpToolSync();
|
|
5214
5682
|
} else if (!ownsMcpManager) {
|
|
5215
5683
|
mcpManager.setOnToolsChanged(tools => {
|
|
5216
|
-
void session.refreshMCPTools(tools);
|
|
5684
|
+
void session.refreshMCPTools(tools.map(tool => tool as CustomTool));
|
|
5217
5685
|
});
|
|
5218
5686
|
}
|
|
5219
5687
|
wireOwnedMcpManagerLifecycle();
|
|
5220
5688
|
}
|
|
5689
|
+
await wireInheritedMcpToolSync();
|
|
5221
5690
|
|
|
5222
5691
|
// Constructor-time workflow-gate tool restoration is deferred by one
|
|
5223
5692
|
// microtask (the ToolSession closure needs `session` assigned). Await it
|
|
@@ -5289,6 +5758,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
5289
5758
|
conventional.sources,
|
|
5290
5759
|
conventional.conventionalConfigs,
|
|
5291
5760
|
new Set<string>(),
|
|
5761
|
+
new Map<string, GjcPluginMcpServerProvenance>(),
|
|
5762
|
+
conventional.cleanup,
|
|
5292
5763
|
);
|
|
5293
5764
|
const resultTools = result.tools as CustomTool[];
|
|
5294
5765
|
if (!cancelled && !session.isDisposed) {
|
|
@@ -5298,7 +5769,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
5298
5769
|
// before this registration would therefore drop a persisted conventional
|
|
5299
5770
|
// MCP selection and persist the empty set.
|
|
5300
5771
|
await session.refreshMCPTools(resultTools);
|
|
5301
|
-
|
|
5772
|
+
wireOwnedMcpToolSync(resultTools);
|
|
5302
5773
|
wireOwnedMcpManagerLifecycle();
|
|
5303
5774
|
if (
|
|
5304
5775
|
!session.isDisposed &&
|
|
@@ -5356,6 +5827,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
5356
5827
|
} catch (error) {
|
|
5357
5828
|
// Release the subscription if the throw happened after install but before the
|
|
5358
5829
|
// dispose-wrap took ownership.
|
|
5830
|
+
stopInheritedMcpToolsSubscription();
|
|
5359
5831
|
releaseCredentialDisabledSubscription();
|
|
5360
5832
|
let cleanupDiagnostic: unknown;
|
|
5361
5833
|
try {
|