@gajae-code/coding-agent 0.17.4 → 0.17.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -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/lifecycle-ledger.d.ts +11 -3
- 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 +1058 -150
- package/src/sdk/broker/ensure.ts +65 -8
- package/src/sdk/broker/identity.ts +13 -0
- package/src/sdk/broker/lifecycle-ledger.ts +16 -4
- 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 +60 -14
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Container, type SelectItem, SelectList, Spacer, Text } from "@gajae-code/tui";
|
|
1
|
+
import { Container, type SelectItem, SelectList, Spacer, Text, truncateToWidth } from "@gajae-code/tui";
|
|
2
2
|
import {
|
|
3
3
|
applyGjcBundleUpdate,
|
|
4
4
|
type GjcLifecycleContext,
|
|
@@ -20,8 +20,11 @@ import type {
|
|
|
20
20
|
GjcUpdatePreview,
|
|
21
21
|
} from "../../extensibility/gjc-plugins/types";
|
|
22
22
|
import { getSelectListTheme, theme } from "../../modes/theme/theme";
|
|
23
|
+
import { sanitizeStatusText } from "../shared";
|
|
23
24
|
import { DynamicBorder } from "./dynamic-border";
|
|
24
25
|
|
|
26
|
+
const MAX_RUNTIME_FINDING_DISPLAY_WIDTH = 120;
|
|
27
|
+
|
|
25
28
|
/** Injectable lifecycle boundary; Settings never reads registries or executes bundle code. */
|
|
26
29
|
export interface GjcBundleLifecyclePort {
|
|
27
30
|
listGjcBundles(ctx: GjcLifecycleContext): Promise<GjcBundleSummary[]>;
|
|
@@ -244,6 +247,23 @@ export class GjcBundleSettingsComponent extends Container {
|
|
|
244
247
|
),
|
|
245
248
|
);
|
|
246
249
|
}
|
|
250
|
+
const runtime = findingsForBundle(
|
|
251
|
+
this.dependencies.runtimeSnapshotProvider,
|
|
252
|
+
bundle.identity,
|
|
253
|
+
this.dependencies.activationGeneration ?? 0,
|
|
254
|
+
);
|
|
255
|
+
if (runtime.status === "current") {
|
|
256
|
+
for (const finding of runtime.findings) {
|
|
257
|
+
const detail = truncateToWidth(
|
|
258
|
+
sanitizeStatusText(`${finding.surfaceId}: ${finding.message}`),
|
|
259
|
+
MAX_RUNTIME_FINDING_DISPLAY_WIDTH,
|
|
260
|
+
);
|
|
261
|
+
const label = finding.decision === "error" ? "Runtime error" : "Runtime finding";
|
|
262
|
+
this.addChild(
|
|
263
|
+
new Text(theme.fg(finding.decision === "error" ? "error" : "warning", ` ${label}: ${detail}`), 0, 0),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
247
267
|
if (this.#message) this.addChild(new Text(theme.fg("warning", ` ${this.#message}`), 0, 0));
|
|
248
268
|
const actions: SelectItem[] = [];
|
|
249
269
|
if (bundle.enabled || !bundle.quarantined) {
|
|
@@ -41,6 +41,33 @@ import { getSeparator } from "./status-line/separators";
|
|
|
41
41
|
import { calculateTokensPerSecond } from "./status-line/token-rate";
|
|
42
42
|
import type { SeparatorDef } from "./status-line/types";
|
|
43
43
|
|
|
44
|
+
function usageReportAccountId(report: unknown): string | undefined {
|
|
45
|
+
if (!report || typeof report !== "object") return undefined;
|
|
46
|
+
const record = report as {
|
|
47
|
+
metadata?: unknown;
|
|
48
|
+
limits?: unknown;
|
|
49
|
+
};
|
|
50
|
+
const metadata = record.metadata;
|
|
51
|
+
const rawMetadataAccountId =
|
|
52
|
+
metadata && typeof metadata === "object" ? (metadata as { accountId?: unknown }).accountId : undefined;
|
|
53
|
+
const metadataAccountId =
|
|
54
|
+
typeof rawMetadataAccountId === "string" && rawMetadataAccountId.trim() ? rawMetadataAccountId.trim() : undefined;
|
|
55
|
+
if (!Array.isArray(record.limits)) return metadataAccountId;
|
|
56
|
+
|
|
57
|
+
const scopeAccountIds = new Set<string>();
|
|
58
|
+
for (const limit of record.limits) {
|
|
59
|
+
if (!limit || typeof limit !== "object") continue;
|
|
60
|
+
const scope = (limit as { scope?: unknown }).scope;
|
|
61
|
+
if (!scope || typeof scope !== "object") continue;
|
|
62
|
+
const accountId = (scope as { accountId?: unknown }).accountId;
|
|
63
|
+
if (typeof accountId === "string" && accountId.trim()) scopeAccountIds.add(accountId.trim());
|
|
64
|
+
}
|
|
65
|
+
if (scopeAccountIds.size > 1) return undefined;
|
|
66
|
+
const [scopeAccountId] = scopeAccountIds;
|
|
67
|
+
if (metadataAccountId && scopeAccountId && metadataAccountId !== scopeAccountId) return undefined;
|
|
68
|
+
return metadataAccountId ?? scopeAccountId;
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
export interface StatusLineSegmentOptions {
|
|
45
72
|
model?: { showThinkingLevel?: boolean; showContextPercent?: boolean };
|
|
46
73
|
path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
|
|
@@ -218,6 +245,7 @@ export class StatusLineComponent implements Component {
|
|
|
218
245
|
#cachedUsage: SegmentContext["usage"] = null;
|
|
219
246
|
#cachedUsageReports: unknown = null;
|
|
220
247
|
#cachedUsageProvider: string | undefined;
|
|
248
|
+
#cachedUsageAccountId: string | undefined;
|
|
221
249
|
#usageFetchedAt = 0;
|
|
222
250
|
#usageInFlight = false;
|
|
223
251
|
|
|
@@ -702,7 +730,12 @@ export class StatusLineComponent implements Component {
|
|
|
702
730
|
.then(reports => {
|
|
703
731
|
this.#cachedUsageReports = reports;
|
|
704
732
|
this.#cachedUsageProvider = this.#activeUsageProvider();
|
|
705
|
-
this.#
|
|
733
|
+
this.#cachedUsageAccountId = this.#activeCodexAccountId(this.#cachedUsageProvider);
|
|
734
|
+
this.#cachedUsage = this.#normalizeUsageReports(
|
|
735
|
+
reports,
|
|
736
|
+
this.#cachedUsageProvider,
|
|
737
|
+
this.#cachedUsageAccountId,
|
|
738
|
+
);
|
|
706
739
|
this.#usageFetchedAt = Date.now();
|
|
707
740
|
if (this.#onBranchChange) {
|
|
708
741
|
this.#onBranchChange();
|
|
@@ -720,10 +753,25 @@ export class StatusLineComponent implements Component {
|
|
|
720
753
|
});
|
|
721
754
|
}
|
|
722
755
|
|
|
723
|
-
#normalizeUsageReports(
|
|
724
|
-
|
|
725
|
-
|
|
756
|
+
#normalizeUsageReports(
|
|
757
|
+
reports: unknown,
|
|
758
|
+
activeProvider = this.#activeUsageProvider(),
|
|
759
|
+
activeCodexAccountId = this.#activeCodexAccountId(activeProvider),
|
|
760
|
+
): SegmentContext["usage"] {
|
|
726
761
|
if (!activeProvider || !Array.isArray(reports)) return null;
|
|
762
|
+
const activeCodexReportCount =
|
|
763
|
+
activeProvider === "openai-codex" && activeCodexAccountId
|
|
764
|
+
? reports.filter(report => {
|
|
765
|
+
if (!report || typeof report !== "object") return false;
|
|
766
|
+
const provider = (report as { provider?: unknown }).provider;
|
|
767
|
+
return (
|
|
768
|
+
typeof provider === "string" &&
|
|
769
|
+
resolveOAuthStorageProvider(provider) === activeProvider &&
|
|
770
|
+
usageReportAccountId(report) === activeCodexAccountId
|
|
771
|
+
);
|
|
772
|
+
}).length
|
|
773
|
+
: 0;
|
|
774
|
+
const ambiguousActiveCodexReports = activeCodexReportCount > 1;
|
|
727
775
|
const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
|
|
728
776
|
const seen = new Set<string>();
|
|
729
777
|
const now = Date.now();
|
|
@@ -759,6 +807,14 @@ export class StatusLineComponent implements Component {
|
|
|
759
807
|
const provider = (report as { provider?: unknown }).provider;
|
|
760
808
|
const providerId = typeof provider === "string" ? resolveOAuthStorageProvider(provider) : undefined;
|
|
761
809
|
if (providerId !== activeProvider) continue;
|
|
810
|
+
// Codex usage fetches cover every OAuth account; fail closed unless this session's account matches uniquely.
|
|
811
|
+
if (
|
|
812
|
+
providerId === "openai-codex" &&
|
|
813
|
+
(!activeCodexAccountId ||
|
|
814
|
+
ambiguousActiveCodexReports ||
|
|
815
|
+
usageReportAccountId(report) !== activeCodexAccountId)
|
|
816
|
+
)
|
|
817
|
+
continue;
|
|
762
818
|
const limits = (report as { limits?: unknown }).limits;
|
|
763
819
|
if (!Array.isArray(limits)) continue;
|
|
764
820
|
for (const limit of limits) {
|
|
@@ -808,11 +864,21 @@ export class StatusLineComponent implements Component {
|
|
|
808
864
|
return typeof provider === "string" && provider.length > 0 ? resolveOAuthStorageProvider(provider) : undefined;
|
|
809
865
|
}
|
|
810
866
|
|
|
867
|
+
#activeCodexAccountId(activeProvider: string | undefined): string | undefined {
|
|
868
|
+
if (activeProvider !== "openai-codex") return undefined;
|
|
869
|
+
const modelRegistry = this.session.modelRegistry;
|
|
870
|
+
return modelRegistry.authStorage.getOAuthAccountId(activeProvider, this.session.credentialSessionId, {
|
|
871
|
+
owner: modelRegistry.getAuthStorageOwner(),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
811
875
|
#syncCachedUsageForActiveProvider(): void {
|
|
812
876
|
const activeProvider = this.#activeUsageProvider();
|
|
813
|
-
|
|
877
|
+
const activeCodexAccountId = this.#activeCodexAccountId(activeProvider);
|
|
878
|
+
if (this.#cachedUsageProvider === activeProvider && this.#cachedUsageAccountId === activeCodexAccountId) return;
|
|
814
879
|
this.#cachedUsageProvider = activeProvider;
|
|
815
|
-
this.#
|
|
880
|
+
this.#cachedUsageAccountId = activeCodexAccountId;
|
|
881
|
+
this.#cachedUsage = this.#normalizeUsageReports(this.#cachedUsageReports, activeProvider, activeCodexAccountId);
|
|
816
882
|
}
|
|
817
883
|
|
|
818
884
|
#buildSegmentContext(
|
|
@@ -1606,7 +1606,7 @@ export class MCPCommandController {
|
|
|
1606
1606
|
if (!this.ctx.mcpManager) {
|
|
1607
1607
|
return;
|
|
1608
1608
|
}
|
|
1609
|
-
if (this.ctx.mcpManager.
|
|
1609
|
+
if (this.ctx.mcpManager.isConnectionSetMutationBlocked()) {
|
|
1610
1610
|
throw new Error("This session's plugin-bundle MCP connections are fixed for its lifetime.");
|
|
1611
1611
|
}
|
|
1612
1612
|
|
|
@@ -44,10 +44,13 @@ export interface MCPToolsLoadOptions {
|
|
|
44
44
|
sharedPoolIdleMs?: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async function
|
|
47
|
+
export async function resolveMCPToolCache(
|
|
48
|
+
storage?: AgentStorage | null,
|
|
49
|
+
dbPath?: string,
|
|
50
|
+
): Promise<MCPToolCache | null> {
|
|
48
51
|
if (storage === null) return null;
|
|
49
52
|
try {
|
|
50
|
-
const resolved = storage ?? (await AgentStorage.open());
|
|
53
|
+
const resolved = storage ?? (await AgentStorage.open(dbPath));
|
|
51
54
|
return new MCPToolCache(resolved);
|
|
52
55
|
} catch (error) {
|
|
53
56
|
logger.warn("MCP tool cache unavailable", { error: String(error) });
|
|
@@ -63,7 +66,7 @@ async function resolveToolCache(storage: AgentStorage | null | undefined): Promi
|
|
|
63
66
|
* @returns MCP tools in LoadedCustomTool format for integration
|
|
64
67
|
*/
|
|
65
68
|
export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoadOptions): Promise<MCPToolsLoadResult> {
|
|
66
|
-
const toolCache = await
|
|
69
|
+
const toolCache = await resolveMCPToolCache(options?.cacheStorage);
|
|
67
70
|
const manager = new MCPManager(cwd, toolCache, { sharedPoolIdleMs: options?.sharedPoolIdleMs });
|
|
68
71
|
if (options?.authStorage) {
|
|
69
72
|
manager.setAuthStorage(options.authStorage);
|
|
@@ -52,7 +52,7 @@ import type {
|
|
|
52
52
|
MCPServerConnection,
|
|
53
53
|
MCPToolDefinition,
|
|
54
54
|
} from "./types";
|
|
55
|
-
import { MCPExpectedFailure, MCPNotificationMethods } from "./types";
|
|
55
|
+
import { MCPExpectedFailure, MCPHttpRequestError, MCPJsonRpcError, MCPNotificationMethods } from "./types";
|
|
56
56
|
|
|
57
57
|
type ToolLoadResult = {
|
|
58
58
|
connection: MCPServerConnection;
|
|
@@ -158,6 +158,24 @@ export function withinDeclaredConnectionWindow(config: MCPServerConfig, elapsedM
|
|
|
158
158
|
return elapsedMs < timeout;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/** Keep startup logs bounded and free of remote response text. */
|
|
162
|
+
function classifyMCPStartupFailure(error: unknown): string {
|
|
163
|
+
if (error instanceof MCPExpectedFailure) {
|
|
164
|
+
return error.cause === undefined ? "expected-mcp-failure" : classifyMCPStartupFailure(error.cause);
|
|
165
|
+
}
|
|
166
|
+
if (error instanceof MCPHttpRequestError) {
|
|
167
|
+
return Number.isInteger(error.status) && error.status >= 100 && error.status <= 599
|
|
168
|
+
? `http-status:${error.status}`
|
|
169
|
+
: "http-request-error";
|
|
170
|
+
}
|
|
171
|
+
if (error instanceof MCPJsonRpcError) {
|
|
172
|
+
return Number.isInteger(error.code) && Math.abs(error.code) <= 1_000_000
|
|
173
|
+
? `json-rpc-code:${error.code}`
|
|
174
|
+
: "json-rpc-error";
|
|
175
|
+
}
|
|
176
|
+
return "transport-error";
|
|
177
|
+
}
|
|
178
|
+
|
|
161
179
|
function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
|
|
162
180
|
const tracked: TrackedPromise<T> = { promise, status: "pending" };
|
|
163
181
|
promise.then(
|
|
@@ -312,10 +330,10 @@ export interface MCPManagerOptions {
|
|
|
312
330
|
/** Restrict this instance to tools from an explicit MCP config. */
|
|
313
331
|
toolsOnly?: boolean;
|
|
314
332
|
/**
|
|
315
|
-
* Ceiling for the startup wait, in milliseconds.
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
333
|
+
* Ceiling for the startup wait, in milliseconds. ACP lifecycle launches and
|
|
334
|
+
* plugin MCP startup use bounded caller-owned budgets; other consumers keep
|
|
335
|
+
* the short default. Non-positive or non-finite values are ignored and the
|
|
336
|
+
* default applies.
|
|
319
337
|
*/
|
|
320
338
|
maxStartupTimeoutMs?: number;
|
|
321
339
|
/** Connection pool used for every physical MCP open/close. */
|
|
@@ -324,6 +342,8 @@ export interface MCPManagerOptions {
|
|
|
324
342
|
sessionId?: string;
|
|
325
343
|
/** Idle retention for shared pool entries. */
|
|
326
344
|
sharedPoolIdleMs?: number;
|
|
345
|
+
/** Limit tool-cache reads and writes to this manager's conventional servers. */
|
|
346
|
+
toolCacheServerNames?: ReadonlySet<string>;
|
|
327
347
|
/** Test seam for deterministic reconnect backoff scheduling. */
|
|
328
348
|
sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
329
349
|
/** Test seam for fencing the acquisition-to-registration replacement race. */
|
|
@@ -412,7 +432,8 @@ export class MCPManager {
|
|
|
412
432
|
#authStorage: AuthStorage | null = null;
|
|
413
433
|
#inputRequestHandler: MCPInputRequestHandler | null = null;
|
|
414
434
|
#onNotification?: (serverName: string, method: string, params: unknown) => void;
|
|
415
|
-
#onToolsChanged?: (tools: CustomTool<TSchema, MCPToolDetails
|
|
435
|
+
#onToolsChanged?: (tools: readonly Readonly<CustomTool<TSchema, MCPToolDetails>>[]) => void;
|
|
436
|
+
readonly #toolChangeListeners = new Set<(tools: readonly Readonly<CustomTool<TSchema, MCPToolDetails>>[]) => void>();
|
|
416
437
|
#onResourcesChanged?: (serverName: string, uri: string) => void;
|
|
417
438
|
#onPromptsChanged?: (serverName: string) => void;
|
|
418
439
|
#notificationsEnabled = false;
|
|
@@ -438,13 +459,21 @@ export class MCPManager {
|
|
|
438
459
|
#scopedLifecycle: "open" | "disconnecting" | "reconnecting" = "open";
|
|
439
460
|
#scopedLifecycleEpoch = 0;
|
|
440
461
|
readonly #toolsOnly: boolean;
|
|
462
|
+
#toolCacheServerNames: Set<string> | undefined;
|
|
441
463
|
#toolsOnlyConfigLoaded = false;
|
|
464
|
+
#connectionSetMutationBlocked = false;
|
|
442
465
|
#connectionSetSealed = false;
|
|
443
466
|
|
|
444
467
|
#serverError(message: string): string {
|
|
445
468
|
return this.#toolsOnly ? "MCP server unavailable" : message;
|
|
446
469
|
}
|
|
447
470
|
|
|
471
|
+
#shouldCacheServerTools(name: string): boolean {
|
|
472
|
+
return (
|
|
473
|
+
this.toolCache !== null && (this.#toolCacheServerNames === undefined || this.#toolCacheServerNames.has(name))
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
448
477
|
#removePendingAcquireCleanup(name: string, error: MCPPoolAcquireAbortError): void {
|
|
449
478
|
const pending = this.#pendingAcquireCleanups.get(name);
|
|
450
479
|
pending?.delete(error);
|
|
@@ -548,13 +577,27 @@ export class MCPManager {
|
|
|
548
577
|
return this.#scopedLifecycleEpoch;
|
|
549
578
|
}
|
|
550
579
|
#assertConnectionSetMutable(): void {
|
|
551
|
-
if (this.#
|
|
580
|
+
if (this.#connectionSetMutationBlocked) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
this.#connectionSetSealed ? "MCP manager connection set is sealed" : "MCP manager connection set is frozen",
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Block config mutations and reloads while keeping existing servers reconnectable. */
|
|
588
|
+
freezeConnectionSet(): void {
|
|
589
|
+
this.#connectionSetMutationBlocked = true;
|
|
552
590
|
}
|
|
553
591
|
|
|
554
592
|
sealConnectionSet(): void {
|
|
593
|
+
this.freezeConnectionSet();
|
|
555
594
|
this.#connectionSetSealed = true;
|
|
556
595
|
}
|
|
557
596
|
|
|
597
|
+
isConnectionSetMutationBlocked(): boolean {
|
|
598
|
+
return this.#connectionSetMutationBlocked;
|
|
599
|
+
}
|
|
600
|
+
|
|
558
601
|
isConnectionSetSealed(): boolean {
|
|
559
602
|
return this.#connectionSetSealed;
|
|
560
603
|
}
|
|
@@ -951,6 +994,7 @@ export class MCPManager {
|
|
|
951
994
|
options: MCPManagerOptions = {},
|
|
952
995
|
) {
|
|
953
996
|
this.#toolsOnly = options.toolsOnly === true;
|
|
997
|
+
this.#toolCacheServerNames = options.toolCacheServerNames ? new Set(options.toolCacheServerNames) : undefined;
|
|
954
998
|
this.#maxStartupTimeoutMs = options.maxStartupTimeoutMs;
|
|
955
999
|
this.#sleep = options.sleep ?? delay;
|
|
956
1000
|
this.#afterLeaseAcquiredForTests = options.afterLeaseAcquiredForTests;
|
|
@@ -972,13 +1016,46 @@ export class MCPManager {
|
|
|
972
1016
|
}
|
|
973
1017
|
|
|
974
1018
|
/**
|
|
975
|
-
* Set a callback to
|
|
1019
|
+
* Set a callback to receive a read-only catalog snapshot when server tools change.
|
|
976
1020
|
*/
|
|
977
|
-
setOnToolsChanged(handler: (tools: CustomTool<TSchema, MCPToolDetails
|
|
1021
|
+
setOnToolsChanged(handler: (tools: readonly Readonly<CustomTool<TSchema, MCPToolDetails>>[]) => void): void {
|
|
978
1022
|
if (this.#toolsOnly) return;
|
|
979
1023
|
this.#onToolsChanged = handler;
|
|
980
1024
|
}
|
|
981
1025
|
|
|
1026
|
+
/** Subscribe to readonly catalog snapshots without replacing the manager owner's callback. */
|
|
1027
|
+
subscribeToToolsChanged(
|
|
1028
|
+
handler: (tools: readonly Readonly<CustomTool<TSchema, MCPToolDetails>>[]) => void,
|
|
1029
|
+
): () => void {
|
|
1030
|
+
if (this.#toolsOnly) return () => {};
|
|
1031
|
+
this.#toolChangeListeners.add(handler);
|
|
1032
|
+
return () => {
|
|
1033
|
+
this.#toolChangeListeners.delete(handler);
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
#notifyToolsChanged(): void {
|
|
1038
|
+
const listeners = [...this.#toolChangeListeners];
|
|
1039
|
+
// Keep live MCPTool instances for reconnect behavior, but never expose the
|
|
1040
|
+
// manager-owned array; callback types make their tool descriptors readonly.
|
|
1041
|
+
const snapshot = this.#onToolsChanged || listeners.length > 0 ? Object.freeze([...this.#tools]) : undefined;
|
|
1042
|
+
if (snapshot && this.#onToolsChanged) {
|
|
1043
|
+
try {
|
|
1044
|
+
this.#onToolsChanged(snapshot);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
logger.warn("MCP tool catalog owner callback failed", { error: classifyMCPStartupFailure(error) });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (!snapshot) return;
|
|
1050
|
+
for (const listener of listeners) {
|
|
1051
|
+
try {
|
|
1052
|
+
listener(snapshot);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
logger.warn("MCP tool catalog listener failed", { error: classifyMCPStartupFailure(error) });
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
982
1059
|
/**
|
|
983
1060
|
* Set a callback to fire when any server's resources change.
|
|
984
1061
|
*/
|
|
@@ -1107,6 +1184,11 @@ export class MCPManager {
|
|
|
1107
1184
|
nativeOnly: options?.nativeOnly,
|
|
1108
1185
|
configPath: options?.configPath,
|
|
1109
1186
|
});
|
|
1187
|
+
if (this.#toolCacheServerNames !== undefined) {
|
|
1188
|
+
this.#toolCacheServerNames = new Set(
|
|
1189
|
+
Object.keys(configs).filter(name => sources[name]?.provider !== "gjc-plugins"),
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1110
1192
|
const result = await this.#connectServers(configs, sources, options?.onConnecting);
|
|
1111
1193
|
if (configurationWarning) result.errors.set("$config", "MCP configuration unavailable");
|
|
1112
1194
|
result.exaApiKeys = exaApiKeys;
|
|
@@ -1362,8 +1444,9 @@ export class MCPManager {
|
|
|
1362
1444
|
this.#exactToolOptions(name, config.sharing === "shared"),
|
|
1363
1445
|
);
|
|
1364
1446
|
this.#replaceServerTools(name, customTools);
|
|
1365
|
-
if (!this.#toolsOnly) this.#
|
|
1366
|
-
if (!this.#toolsOnly
|
|
1447
|
+
if (!this.#toolsOnly) this.#notifyToolsChanged();
|
|
1448
|
+
if (!this.#toolsOnly && this.#shouldCacheServerTools(name))
|
|
1449
|
+
void this.toolCache?.set(name, config, serverTools);
|
|
1367
1450
|
if (!this.#toolsOnly) await this.#loadServerResourcesAndPrompts(name, connection);
|
|
1368
1451
|
})
|
|
1369
1452
|
.catch(error => {
|
|
@@ -1374,7 +1457,11 @@ export class MCPManager {
|
|
|
1374
1457
|
const message = error instanceof Error ? error.message : String(error);
|
|
1375
1458
|
errors.set(name, this.#serverError(message));
|
|
1376
1459
|
reportedErrors.add(name);
|
|
1377
|
-
logger.error("MCP tool load failed", {
|
|
1460
|
+
logger.error("MCP tool load failed", {
|
|
1461
|
+
path: `mcp:${name}`,
|
|
1462
|
+
serverName: name,
|
|
1463
|
+
error: classifyMCPStartupFailure(error),
|
|
1464
|
+
});
|
|
1378
1465
|
});
|
|
1379
1466
|
}
|
|
1380
1467
|
|
|
@@ -1430,6 +1517,7 @@ export class MCPManager {
|
|
|
1430
1517
|
if (this.toolCache && !this.#toolsOnly) {
|
|
1431
1518
|
await Promise.all(
|
|
1432
1519
|
pendingTasks.map(async task => {
|
|
1520
|
+
if (!this.#shouldCacheServerTools(task.name)) return;
|
|
1433
1521
|
const cached = await this.toolCache?.get(task.name, task.config);
|
|
1434
1522
|
if (cached) {
|
|
1435
1523
|
cachedTools.set(task.name, cached);
|
|
@@ -1438,51 +1526,55 @@ export class MCPManager {
|
|
|
1438
1526
|
);
|
|
1439
1527
|
}
|
|
1440
1528
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
task.connectionAbort.abort(new Error(message));
|
|
1475
|
-
if (this.#pendingConnections.has(task.name)) this.#pendingConnections.delete(task.name);
|
|
1476
|
-
if (this.#pendingToolLoads.get(task.name) === task.toolsPromise)
|
|
1477
|
-
this.#pendingToolLoads.delete(task.name);
|
|
1478
|
-
this.#pendingConnectionControllers.delete(task.name);
|
|
1479
|
-
void this.#disconnectServer(task.name, { preserveConfig: this.#toolsOnly }).catch(error => {
|
|
1480
|
-
this.#logLeaseReleaseFailure(task.name, undefined, error);
|
|
1529
|
+
// Cached tools are only a fallback snapshot; they do not exempt the live
|
|
1530
|
+
// connection from the startup cleanup contract. The startup wait elapsing
|
|
1531
|
+
// means "stop blocking session start", not "this server failed". A server
|
|
1532
|
+
// whose operator declared a `timeout` (`gjc mcp add --timeout`) asked to
|
|
1533
|
+
// wait that long for it, so while it is still inside that window it keeps
|
|
1534
|
+
// connecting in the background under `connectToServer`'s own timeout, and
|
|
1535
|
+
// the background tool load adopts it. Only a server that declared no
|
|
1536
|
+
// window, or already spent it, is torn down and reported. Exact-config
|
|
1537
|
+
// (`toolsOnly`) startup still fails fast: it builds a catalog once, so a
|
|
1538
|
+
// missing server is an error.
|
|
1539
|
+
// Abort and disconnect in the background: a misbehaving stdio/MCP transport can
|
|
1540
|
+
// ignore AbortSignal and keep startup blocked indefinitely, but it must not remain
|
|
1541
|
+
// registered if it eventually connects.
|
|
1542
|
+
const startupElapsedMs = Date.now() - startupStartedAt;
|
|
1543
|
+
for (const task of pendingTasks) {
|
|
1544
|
+
if (task.tracked.status !== "pending") continue;
|
|
1545
|
+
if (!this.#toolsOnly && withinDeclaredConnectionWindow(task.config, startupElapsedMs)) {
|
|
1546
|
+
logger.warn("MCP server still connecting after the startup wait", {
|
|
1547
|
+
path: `mcp:${task.name}`,
|
|
1548
|
+
serverName: task.name,
|
|
1549
|
+
startupWaitMs: startupTimeoutMs,
|
|
1550
|
+
declaredTimeoutMs: task.config.timeout,
|
|
1551
|
+
});
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
const message = `MCP server connection timed out during startup: ${task.name}`;
|
|
1555
|
+
if (!this.#toolsOnly) {
|
|
1556
|
+
logger.warn("MCP server connection timed out during startup", {
|
|
1557
|
+
path: `mcp:${task.name}`,
|
|
1558
|
+
serverName: task.name,
|
|
1559
|
+
startupWaitMs: startupTimeoutMs,
|
|
1560
|
+
declaredTimeoutMs: task.config.timeout,
|
|
1561
|
+
remediation: "Set a per-server timeout with `gjc mcp add --timeout <ms>` for slower servers.",
|
|
1481
1562
|
});
|
|
1482
1563
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1564
|
+
errors.set(task.name, this.#serverError(message));
|
|
1565
|
+
reportedErrors.add(task.name);
|
|
1566
|
+
task.connectionAbort.abort(new Error(message));
|
|
1567
|
+
if (this.#pendingConnections.has(task.name)) this.#pendingConnections.delete(task.name);
|
|
1568
|
+
if (this.#pendingToolLoads.get(task.name) === task.toolsPromise)
|
|
1569
|
+
this.#pendingToolLoads.delete(task.name);
|
|
1570
|
+
this.#pendingConnectionControllers.delete(task.name);
|
|
1571
|
+
void this.#disconnectServer(task.name, {
|
|
1572
|
+
preserveConfig: this.#toolsOnly || cachedTools.has(task.name),
|
|
1573
|
+
preserveSource: cachedTools.has(task.name),
|
|
1574
|
+
preserveTools: cachedTools.has(task.name),
|
|
1575
|
+
}).catch(error => {
|
|
1576
|
+
this.#logLeaseReleaseFailure(task.name, undefined, error);
|
|
1577
|
+
});
|
|
1486
1578
|
}
|
|
1487
1579
|
}
|
|
1488
1580
|
|
|
@@ -1524,7 +1616,7 @@ export class MCPManager {
|
|
|
1524
1616
|
logger.warn("MCP server connection failed during startup", {
|
|
1525
1617
|
path: `mcp:${name}`,
|
|
1526
1618
|
serverName: name,
|
|
1527
|
-
error:
|
|
1619
|
+
error: classifyMCPStartupFailure(reason),
|
|
1528
1620
|
remediation:
|
|
1529
1621
|
"Check the server command and set --timeout <ms> when startup is expected to be slow.",
|
|
1530
1622
|
});
|
|
@@ -1573,7 +1665,10 @@ export class MCPManager {
|
|
|
1573
1665
|
}
|
|
1574
1666
|
|
|
1575
1667
|
// Update cached tools
|
|
1576
|
-
if (shouldPublishToolSnapshot)
|
|
1668
|
+
if (shouldPublishToolSnapshot) {
|
|
1669
|
+
this.#publishToolCatalog(allTools);
|
|
1670
|
+
this.#notifyToolsChanged();
|
|
1671
|
+
}
|
|
1577
1672
|
allowBackgroundLogging = true;
|
|
1578
1673
|
|
|
1579
1674
|
return {
|
|
@@ -2386,7 +2481,10 @@ export class MCPManager {
|
|
|
2386
2481
|
await this.#disconnectServer(name);
|
|
2387
2482
|
}
|
|
2388
2483
|
|
|
2389
|
-
async #disconnectServer(
|
|
2484
|
+
async #disconnectServer(
|
|
2485
|
+
name: string,
|
|
2486
|
+
options: { preserveConfig?: boolean; preserveSource?: boolean; preserveTools?: boolean } = {},
|
|
2487
|
+
): Promise<void> {
|
|
2390
2488
|
const nextEpoch = (this.#disconnectEpochs.get(name) ?? 0) + 1;
|
|
2391
2489
|
this.#disconnectEpochs.set(name, nextEpoch);
|
|
2392
2490
|
this.#pendingConnectionControllers.get(name)?.abort(new Error(`MCP server disconnected: ${name}`));
|
|
@@ -2396,7 +2494,7 @@ export class MCPManager {
|
|
|
2396
2494
|
this.#pendingConnections.delete(name);
|
|
2397
2495
|
this.#pendingToolLoads.delete(name);
|
|
2398
2496
|
this.#pendingReconnections.delete(name);
|
|
2399
|
-
this.#sources.delete(name);
|
|
2497
|
+
if (!options.preserveSource) this.#sources.delete(name);
|
|
2400
2498
|
if (!options.preserveConfig) this.#serverConfigs.delete(name);
|
|
2401
2499
|
this.#pendingResourceRefresh.delete(name);
|
|
2402
2500
|
const connection = this.#connections.get(name);
|
|
@@ -2426,18 +2524,20 @@ export class MCPManager {
|
|
|
2426
2524
|
if (this.#connections.get(name) === connection) this.#connections.delete(name);
|
|
2427
2525
|
}
|
|
2428
2526
|
|
|
2429
|
-
//
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2527
|
+
// Keep a published cache fallback available while its expired live connection closes.
|
|
2528
|
+
if (!options.preserveTools) {
|
|
2529
|
+
const hadTools = this.#tools.some(
|
|
2530
|
+
tool => (tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name,
|
|
2531
|
+
);
|
|
2532
|
+
const remainingTools = this.#tools.filter(
|
|
2533
|
+
tool => !((tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name),
|
|
2534
|
+
);
|
|
2535
|
+
if (hadTools) {
|
|
2536
|
+
this.#publishToolCatalog(remainingTools);
|
|
2537
|
+
this.#notifyToolsChanged();
|
|
2538
|
+
} else {
|
|
2539
|
+
this.#tools = remainingTools;
|
|
2540
|
+
}
|
|
2441
2541
|
}
|
|
2442
2542
|
|
|
2443
2543
|
// Notify prompt consumers so stale commands are cleared
|
|
@@ -2545,7 +2645,9 @@ export class MCPManager {
|
|
|
2545
2645
|
this.#pendingExactSuspensions.clear();
|
|
2546
2646
|
this.#pendingExactControlServers.clear();
|
|
2547
2647
|
this.#connections.clear();
|
|
2648
|
+
const hadTools = this.#tools.length > 0;
|
|
2548
2649
|
this.#publishToolCatalog([]);
|
|
2650
|
+
if (hadTools) this.#notifyToolsChanged();
|
|
2549
2651
|
this.#subscribedResources.clear();
|
|
2550
2652
|
const releaseFailures = [
|
|
2551
2653
|
...scopedReleaseFailures,
|
|
@@ -2835,9 +2937,9 @@ export class MCPManager {
|
|
|
2835
2937
|
reconnect,
|
|
2836
2938
|
this.#exactToolOptions(name, config.sharing === "shared"),
|
|
2837
2939
|
);
|
|
2838
|
-
void this.toolCache?.set(name, config, serverTools);
|
|
2940
|
+
if (this.#shouldCacheServerTools(name)) void this.toolCache?.set(name, config, serverTools);
|
|
2839
2941
|
this.#replaceServerTools(name, customTools);
|
|
2840
|
-
this.#
|
|
2942
|
+
this.#notifyToolsChanged();
|
|
2841
2943
|
void this.#loadServerResourcesAndPrompts(name, connection);
|
|
2842
2944
|
return connection;
|
|
2843
2945
|
} catch (error) {
|
|
@@ -2905,11 +3007,11 @@ export class MCPManager {
|
|
|
2905
3007
|
reconnect,
|
|
2906
3008
|
this.#exactToolOptions(name, connection.config.sharing === "shared"),
|
|
2907
3009
|
);
|
|
2908
|
-
void this.toolCache?.set(name, connection.config, serverTools);
|
|
3010
|
+
if (this.#shouldCacheServerTools(name)) void this.toolCache?.set(name, connection.config, serverTools);
|
|
2909
3011
|
|
|
2910
3012
|
// Replace tools from this server
|
|
2911
3013
|
this.#replaceServerTools(name, customTools);
|
|
2912
|
-
this.#
|
|
3014
|
+
this.#notifyToolsChanged();
|
|
2913
3015
|
}
|
|
2914
3016
|
|
|
2915
3017
|
/**
|