agentfootprint 12.0.1 → 12.2.0
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/AGENTS.md +1 -1
- package/CAPABILITIES.md +3 -0
- package/ai-instructions/claude-code/SKILL.md +1 -1
- package/dist/adapters/mcp/agentcoreRuntime/invoke.js +113 -7
- package/dist/adapters/mcp/agentcoreRuntime/invoke.js.map +1 -1
- package/dist/adapters/mcp/agentcoreRuntime/session.js +21 -21
- package/dist/adapters/mcp/agentcoreRuntime/session.js.map +1 -1
- package/dist/adapters/mcp/agentcoreRuntime/transport.js +79 -5
- package/dist/adapters/mcp/agentcoreRuntime/transport.js.map +1 -1
- package/dist/core/Agent.js +6 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/agent/stages/toolCalls.js +4 -0
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/slots/buildToolsSlot.js +8 -0
- package/dist/core/slots/buildToolsSlot.js.map +1 -1
- package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.d.ts +23 -4
- package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.js +112 -6
- package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.js.map +1 -1
- package/dist/esm/adapters/mcp/agentcoreRuntime/session.d.ts +20 -6
- package/dist/esm/adapters/mcp/agentcoreRuntime/session.js +21 -21
- package/dist/esm/adapters/mcp/agentcoreRuntime/session.js.map +1 -1
- package/dist/esm/adapters/mcp/agentcoreRuntime/transport.js +81 -7
- package/dist/esm/adapters/mcp/agentcoreRuntime/transport.js.map +1 -1
- package/dist/esm/core/Agent.js +6 -0
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/agent/stages/toolCalls.js +4 -0
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/slots/buildToolsSlot.d.ts +12 -0
- package/dist/esm/core/slots/buildToolsSlot.js +8 -0
- package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
- package/dist/esm/events/payloads.d.ts +41 -0
- package/dist/esm/events/registry.d.ts +3 -1
- package/dist/esm/events/registry.js +2 -0
- package/dist/esm/events/registry.js.map +1 -1
- package/dist/esm/identity/types.d.ts +7 -3
- package/dist/esm/identity/types.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/boundedJson.d.ts +14 -7
- package/dist/esm/lib/boundedJson.js +14 -7
- package/dist/esm/lib/boundedJson.js.map +1 -1
- package/dist/esm/mcp/callChecks.d.ts +160 -0
- package/dist/esm/mcp/callChecks.js +237 -0
- package/dist/esm/mcp/callChecks.js.map +1 -0
- package/dist/esm/mcp/callLifetime.js +5 -2
- package/dist/esm/mcp/callLifetime.js.map +1 -1
- package/dist/esm/mcp/clientPort.d.ts +78 -0
- package/dist/esm/mcp/clientPort.js +33 -0
- package/dist/esm/mcp/clientPort.js.map +1 -0
- package/dist/esm/mcp/gatewayTransport.js +13 -3
- package/dist/esm/mcp/gatewayTransport.js.map +1 -1
- package/dist/esm/mcp/index.d.ts +11 -1
- package/dist/esm/mcp/index.js +15 -0
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/mcp/listingGenerations.d.ts +54 -0
- package/dist/esm/mcp/listingGenerations.js +75 -0
- package/dist/esm/mcp/listingGenerations.js.map +1 -0
- package/dist/esm/mcp/mcpClient.d.ts +18 -1
- package/dist/esm/mcp/mcpClient.js +302 -57
- package/dist/esm/mcp/mcpClient.js.map +1 -1
- package/dist/esm/mcp/mockMcpClient.js +13 -0
- package/dist/esm/mcp/mockMcpClient.js.map +1 -1
- package/dist/esm/mcp/pool/boundTool.d.ts +58 -0
- package/dist/esm/mcp/pool/boundTool.js +218 -0
- package/dist/esm/mcp/pool/boundTool.js.map +1 -0
- package/dist/esm/mcp/pool/clientSet.d.ts +73 -0
- package/dist/esm/mcp/pool/clientSet.js +152 -0
- package/dist/esm/mcp/pool/clientSet.js.map +1 -0
- package/dist/esm/mcp/pool/definitions.d.ts +103 -0
- package/dist/esm/mcp/pool/definitions.js +168 -0
- package/dist/esm/mcp/pool/definitions.js.map +1 -0
- package/dist/esm/mcp/pool/errors.d.ts +76 -0
- package/dist/esm/mcp/pool/errors.js +91 -0
- package/dist/esm/mcp/pool/errors.js.map +1 -0
- package/dist/esm/mcp/pool/identityKey.d.ts +57 -0
- package/dist/esm/mcp/pool/identityKey.js +94 -0
- package/dist/esm/mcp/pool/identityKey.js.map +1 -0
- package/dist/esm/mcp/pool/lease.d.ts +36 -0
- package/dist/esm/mcp/pool/lease.js +51 -0
- package/dist/esm/mcp/pool/lease.js.map +1 -0
- package/dist/esm/mcp/pool/listTools.d.ts +33 -0
- package/dist/esm/mcp/pool/listTools.js +146 -0
- package/dist/esm/mcp/pool/listTools.js.map +1 -0
- package/dist/esm/mcp/pool/mcpClientPool.d.ts +121 -0
- package/dist/esm/mcp/pool/mcpClientPool.js +151 -0
- package/dist/esm/mcp/pool/mcpClientPool.js.map +1 -0
- package/dist/esm/mcp/pool/options.d.ts +183 -0
- package/dist/esm/mcp/pool/options.js +119 -0
- package/dist/esm/mcp/pool/options.js.map +1 -0
- package/dist/esm/mcp/pool/plan.d.ts +77 -0
- package/dist/esm/mcp/pool/plan.js +134 -0
- package/dist/esm/mcp/pool/plan.js.map +1 -0
- package/dist/esm/mcp/pool/pool.d.ts +307 -0
- package/dist/esm/mcp/pool/pool.js +1071 -0
- package/dist/esm/mcp/pool/pool.js.map +1 -0
- package/dist/esm/mcp/pool/pooledClient.d.ts +93 -0
- package/dist/esm/mcp/pool/pooledClient.js +90 -0
- package/dist/esm/mcp/pool/pooledClient.js.map +1 -0
- package/dist/esm/mcp/pool/record.d.ts +80 -0
- package/dist/esm/mcp/pool/record.js +75 -0
- package/dist/esm/mcp/pool/record.js.map +1 -0
- package/dist/esm/mcp/pool/sessionEnded.d.ts +24 -0
- package/dist/esm/mcp/pool/sessionEnded.js +31 -0
- package/dist/esm/mcp/pool/sessionEnded.js.map +1 -0
- package/dist/esm/mcp/pool/work.d.ts +116 -0
- package/dist/esm/mcp/pool/work.js +213 -0
- package/dist/esm/mcp/pool/work.js.map +1 -0
- package/dist/esm/mcp/requestGuard.d.ts +70 -0
- package/dist/esm/mcp/requestGuard.js +98 -0
- package/dist/esm/mcp/requestGuard.js.map +1 -0
- package/dist/esm/mcp/requestLedger.d.ts +35 -1
- package/dist/esm/mcp/requestLedger.js +76 -3
- package/dist/esm/mcp/requestLedger.js.map +1 -1
- package/dist/esm/mcp/seriesEvidence.d.ts +10 -6
- package/dist/esm/mcp/seriesEvidence.js +16 -7
- package/dist/esm/mcp/seriesEvidence.js.map +1 -1
- package/dist/esm/mcp/terminate.d.ts +94 -0
- package/dist/esm/mcp/terminate.js +184 -0
- package/dist/esm/mcp/terminate.js.map +1 -0
- package/dist/esm/mcp/throttleRetry.js +71 -7
- package/dist/esm/mcp/throttleRetry.js.map +1 -1
- package/dist/esm/mcp/toolList.d.ts +90 -0
- package/dist/esm/mcp/toolList.js +123 -0
- package/dist/esm/mcp/toolList.js.map +1 -0
- package/dist/esm/mcp/types.d.ts +197 -3
- package/dist/esm/redaction/knownStrings.js +34 -0
- package/dist/esm/redaction/knownStrings.js.map +1 -1
- package/dist/esm/tool-providers/index.d.ts +2 -2
- package/dist/esm/tool-providers/index.js +15 -1
- package/dist/esm/tool-providers/index.js.map +1 -1
- package/dist/esm/tools/mcpClientUse.d.ts +59 -0
- package/dist/esm/tools/mcpClientUse.js +216 -0
- package/dist/esm/tools/mcpClientUse.js.map +1 -0
- package/dist/esm/tools/toolProvider.d.ts +16 -0
- package/dist/esm/tools/toolSessions.d.ts +41 -0
- package/dist/esm/tools/toolSessions.js +41 -1
- package/dist/esm/tools/toolSessions.js.map +1 -1
- package/dist/events/registry.js +2 -0
- package/dist/events/registry.js.map +1 -1
- package/dist/identity/types.js.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/boundedJson.js +14 -7
- package/dist/lib/boundedJson.js.map +1 -1
- package/dist/mcp/callChecks.js +247 -0
- package/dist/mcp/callChecks.js.map +1 -0
- package/dist/mcp/callLifetime.js +5 -2
- package/dist/mcp/callLifetime.js.map +1 -1
- package/dist/mcp/clientPort.js +38 -0
- package/dist/mcp/clientPort.js.map +1 -0
- package/dist/mcp/gatewayTransport.js +13 -3
- package/dist/mcp/gatewayTransport.js.map +1 -1
- package/dist/mcp/index.js +25 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/listingGenerations.js +80 -0
- package/dist/mcp/listingGenerations.js.map +1 -0
- package/dist/mcp/mcpClient.js +306 -59
- package/dist/mcp/mcpClient.js.map +1 -1
- package/dist/mcp/mockMcpClient.js +13 -0
- package/dist/mcp/mockMcpClient.js.map +1 -1
- package/dist/mcp/pool/boundTool.js +223 -0
- package/dist/mcp/pool/boundTool.js.map +1 -0
- package/dist/mcp/pool/clientSet.js +157 -0
- package/dist/mcp/pool/clientSet.js.map +1 -0
- package/dist/mcp/pool/definitions.js +174 -0
- package/dist/mcp/pool/definitions.js.map +1 -0
- package/dist/mcp/pool/errors.js +96 -0
- package/dist/mcp/pool/errors.js.map +1 -0
- package/dist/mcp/pool/identityKey.js +99 -0
- package/dist/mcp/pool/identityKey.js.map +1 -0
- package/dist/mcp/pool/lease.js +55 -0
- package/dist/mcp/pool/lease.js.map +1 -0
- package/dist/mcp/pool/listTools.js +150 -0
- package/dist/mcp/pool/listTools.js.map +1 -0
- package/dist/mcp/pool/mcpClientPool.js +155 -0
- package/dist/mcp/pool/mcpClientPool.js.map +1 -0
- package/dist/mcp/pool/options.js +123 -0
- package/dist/mcp/pool/options.js.map +1 -0
- package/dist/mcp/pool/plan.js +140 -0
- package/dist/mcp/pool/plan.js.map +1 -0
- package/dist/mcp/pool/pool.js +1075 -0
- package/dist/mcp/pool/pool.js.map +1 -0
- package/dist/mcp/pool/pooledClient.js +94 -0
- package/dist/mcp/pool/pooledClient.js.map +1 -0
- package/dist/mcp/pool/record.js +80 -0
- package/dist/mcp/pool/record.js.map +1 -0
- package/dist/mcp/pool/sessionEnded.js +35 -0
- package/dist/mcp/pool/sessionEnded.js.map +1 -0
- package/dist/mcp/pool/work.js +219 -0
- package/dist/mcp/pool/work.js.map +1 -0
- package/dist/mcp/requestGuard.js +106 -0
- package/dist/mcp/requestGuard.js.map +1 -0
- package/dist/mcp/requestLedger.js +76 -3
- package/dist/mcp/requestLedger.js.map +1 -1
- package/dist/mcp/seriesEvidence.js +16 -7
- package/dist/mcp/seriesEvidence.js.map +1 -1
- package/dist/mcp/terminate.js +193 -0
- package/dist/mcp/terminate.js.map +1 -0
- package/dist/mcp/throttleRetry.js +71 -7
- package/dist/mcp/throttleRetry.js.map +1 -1
- package/dist/mcp/toolList.js +128 -0
- package/dist/mcp/toolList.js.map +1 -0
- package/dist/redaction/knownStrings.js +34 -0
- package/dist/redaction/knownStrings.js.map +1 -1
- package/dist/tool-providers/index.js +20 -1
- package/dist/tool-providers/index.js.map +1 -1
- package/dist/tools/mcpClientUse.js +225 -0
- package/dist/tools/mcpClientUse.js.map +1 -0
- package/dist/tools/toolSessions.js +44 -1
- package/dist/tools/toolSessions.js.map +1 -1
- package/dist/types/adapters/mcp/agentcoreRuntime/invoke.d.ts +23 -4
- package/dist/types/adapters/mcp/agentcoreRuntime/invoke.d.ts.map +1 -1
- package/dist/types/adapters/mcp/agentcoreRuntime/session.d.ts +20 -6
- package/dist/types/adapters/mcp/agentcoreRuntime/session.d.ts.map +1 -1
- package/dist/types/adapters/mcp/agentcoreRuntime/transport.d.ts.map +1 -1
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/slots/buildToolsSlot.d.ts +12 -0
- package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
- package/dist/types/events/payloads.d.ts +41 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/events/registry.d.ts +3 -1
- package/dist/types/events/registry.d.ts.map +1 -1
- package/dist/types/identity/types.d.ts +7 -3
- package/dist/types/identity/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/boundedJson.d.ts +14 -7
- package/dist/types/lib/boundedJson.d.ts.map +1 -1
- package/dist/types/mcp/callChecks.d.ts +161 -0
- package/dist/types/mcp/callChecks.d.ts.map +1 -0
- package/dist/types/mcp/callLifetime.d.ts.map +1 -1
- package/dist/types/mcp/clientPort.d.ts +79 -0
- package/dist/types/mcp/clientPort.d.ts.map +1 -0
- package/dist/types/mcp/gatewayTransport.d.ts.map +1 -1
- package/dist/types/mcp/index.d.ts +11 -1
- package/dist/types/mcp/index.d.ts.map +1 -1
- package/dist/types/mcp/listingGenerations.d.ts +55 -0
- package/dist/types/mcp/listingGenerations.d.ts.map +1 -0
- package/dist/types/mcp/mcpClient.d.ts +18 -1
- package/dist/types/mcp/mcpClient.d.ts.map +1 -1
- package/dist/types/mcp/mockMcpClient.d.ts.map +1 -1
- package/dist/types/mcp/pool/boundTool.d.ts +59 -0
- package/dist/types/mcp/pool/boundTool.d.ts.map +1 -0
- package/dist/types/mcp/pool/clientSet.d.ts +74 -0
- package/dist/types/mcp/pool/clientSet.d.ts.map +1 -0
- package/dist/types/mcp/pool/definitions.d.ts +104 -0
- package/dist/types/mcp/pool/definitions.d.ts.map +1 -0
- package/dist/types/mcp/pool/errors.d.ts +77 -0
- package/dist/types/mcp/pool/errors.d.ts.map +1 -0
- package/dist/types/mcp/pool/identityKey.d.ts +58 -0
- package/dist/types/mcp/pool/identityKey.d.ts.map +1 -0
- package/dist/types/mcp/pool/lease.d.ts +37 -0
- package/dist/types/mcp/pool/lease.d.ts.map +1 -0
- package/dist/types/mcp/pool/listTools.d.ts +34 -0
- package/dist/types/mcp/pool/listTools.d.ts.map +1 -0
- package/dist/types/mcp/pool/mcpClientPool.d.ts +122 -0
- package/dist/types/mcp/pool/mcpClientPool.d.ts.map +1 -0
- package/dist/types/mcp/pool/options.d.ts +184 -0
- package/dist/types/mcp/pool/options.d.ts.map +1 -0
- package/dist/types/mcp/pool/plan.d.ts +78 -0
- package/dist/types/mcp/pool/plan.d.ts.map +1 -0
- package/dist/types/mcp/pool/pool.d.ts +308 -0
- package/dist/types/mcp/pool/pool.d.ts.map +1 -0
- package/dist/types/mcp/pool/pooledClient.d.ts +94 -0
- package/dist/types/mcp/pool/pooledClient.d.ts.map +1 -0
- package/dist/types/mcp/pool/record.d.ts +81 -0
- package/dist/types/mcp/pool/record.d.ts.map +1 -0
- package/dist/types/mcp/pool/sessionEnded.d.ts +25 -0
- package/dist/types/mcp/pool/sessionEnded.d.ts.map +1 -0
- package/dist/types/mcp/pool/work.d.ts +117 -0
- package/dist/types/mcp/pool/work.d.ts.map +1 -0
- package/dist/types/mcp/requestGuard.d.ts +71 -0
- package/dist/types/mcp/requestGuard.d.ts.map +1 -0
- package/dist/types/mcp/requestLedger.d.ts +35 -1
- package/dist/types/mcp/requestLedger.d.ts.map +1 -1
- package/dist/types/mcp/seriesEvidence.d.ts +10 -6
- package/dist/types/mcp/seriesEvidence.d.ts.map +1 -1
- package/dist/types/mcp/terminate.d.ts +95 -0
- package/dist/types/mcp/terminate.d.ts.map +1 -0
- package/dist/types/mcp/throttleRetry.d.ts.map +1 -1
- package/dist/types/mcp/toolList.d.ts +91 -0
- package/dist/types/mcp/toolList.d.ts.map +1 -0
- package/dist/types/mcp/types.d.ts +197 -3
- package/dist/types/mcp/types.d.ts.map +1 -1
- package/dist/types/redaction/knownStrings.d.ts.map +1 -1
- package/dist/types/tool-providers/index.d.ts +2 -2
- package/dist/types/tool-providers/index.d.ts.map +1 -1
- package/dist/types/tools/mcpClientUse.d.ts +60 -0
- package/dist/types/tools/mcpClientUse.d.ts.map +1 -0
- package/dist/types/tools/toolProvider.d.ts +16 -0
- package/dist/types/tools/toolProvider.d.ts.map +1 -1
- package/dist/types/tools/toolSessions.d.ts +41 -0
- package/dist/types/tools/toolSessions.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* pool — the state behind `mcpClientPool`: who holds which client, what is
|
|
4
|
+
* charged, and what ends when.
|
|
5
|
+
*
|
|
6
|
+
* Pattern: one state machine, mutated only between awaits.
|
|
7
|
+
* Role: §§ 5–7 of the design. Every mutation of the sets and the budget runs
|
|
8
|
+
* synchronously — a check and the move it allows are never split by
|
|
9
|
+
* an await — so two callers cannot both take the last slot.
|
|
10
|
+
*
|
|
11
|
+
* - ADMISSION: every listing, call, acquire and ending is admitted against
|
|
12
|
+
* `maxOperations` before anything is sent. At each interaction the lazy
|
|
13
|
+
* sweep (revoked leases, idle ends, deferred ends) runs FIRST, so it
|
|
14
|
+
* reserves its endings before the new operation is admitted.
|
|
15
|
+
* - ONE OPEN PER KEY, from a plan resolved at that moment, under the pool's own
|
|
16
|
+
* signal and `initTimeoutMs`, never a caller's. A caller that gives up
|
|
17
|
+
* detaches alone; the operation that started the open keeps its charge until
|
|
18
|
+
* the open's work settles. Past the deadline every waiter is refused at once
|
|
19
|
+
* (`init-timeout`), and until the open's work settles the key cannot open
|
|
20
|
+
* again. A failure reaches every current waiter and is not cached.
|
|
21
|
+
* - THE HANDOFF: every operation waiting for an open holds a RESERVATION on
|
|
22
|
+
* the client until it takes its lease, so between the open settling and
|
|
23
|
+
* that lease the client is busy — nobody can evict, idle-end or end it. The
|
|
24
|
+
* client's state is checked again after every wait, and the lease taken in
|
|
25
|
+
* the same step: a lease is never handed out on a client that is ending.
|
|
26
|
+
* - MAKING ROOM: the principal's, then the pool's, least recently used IDLE
|
|
27
|
+
* client moves to the ending set — or `PoolFullError` at once.
|
|
28
|
+
* - ENDING reserves an ending slot and an operation slot FIRST; when either is
|
|
29
|
+
* full the client stays in the open set, counted, reported deferred, and is
|
|
30
|
+
* ended when slots free. Every end goes through `terminate()` — but a client
|
|
31
|
+
* whose handle this process lost: nothing can be sent for it, so its ending
|
|
32
|
+
* is its local close alone, charged the same way until it settles.
|
|
33
|
+
* - SETTLEMENT, never an abort, returns a slot (`work.ts`).
|
|
34
|
+
* - NOTHING UNCHECKED: on a pooled client every request carries a guard — its
|
|
35
|
+
* call's, or the client's open, listing or ending — and a request with none
|
|
36
|
+
* is refused before it is sent (`request-unmatched`), at the watched `send`
|
|
37
|
+
* and at the pool's own fetch alike.
|
|
38
|
+
*/
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.Pool = void 0;
|
|
41
|
+
const abort_js_1 = require("../../lib/abort.js");
|
|
42
|
+
const toolSessions_js_1 = require("../../tools/toolSessions.js");
|
|
43
|
+
const clientPort_js_1 = require("../clientPort.js");
|
|
44
|
+
const mcpClient_js_1 = require("../mcpClient.js");
|
|
45
|
+
const requestGuard_js_1 = require("../requestGuard.js");
|
|
46
|
+
const clientSet_js_1 = require("./clientSet.js");
|
|
47
|
+
const definitions_js_1 = require("./definitions.js");
|
|
48
|
+
const errors_js_1 = require("./errors.js");
|
|
49
|
+
const encode_js_1 = require("../../memory/identity/encode.js");
|
|
50
|
+
const identityKey_js_1 = require("./identityKey.js");
|
|
51
|
+
const options_js_1 = require("./options.js");
|
|
52
|
+
const plan_js_1 = require("./plan.js");
|
|
53
|
+
const pooledClient_js_1 = require("./pooledClient.js");
|
|
54
|
+
const record_js_1 = require("./record.js");
|
|
55
|
+
const work_js_1 = require("./work.js");
|
|
56
|
+
class Pool {
|
|
57
|
+
settings;
|
|
58
|
+
clients = new clientSet_js_1.ClientSet();
|
|
59
|
+
budget;
|
|
60
|
+
shared = new definitions_js_1.SharedDefinitions();
|
|
61
|
+
reporter;
|
|
62
|
+
/** The pool's own signal: what every open, listing and call runs under; aborted at `close()`. */
|
|
63
|
+
lifetime = new AbortController();
|
|
64
|
+
leases = new Set();
|
|
65
|
+
settleWaiters = [];
|
|
66
|
+
/** Waiting for some client to become idle (`terminate`, `close`). */
|
|
67
|
+
idleWaiters = new Set();
|
|
68
|
+
closed = false;
|
|
69
|
+
unresolvedEndings = 0;
|
|
70
|
+
constructor(settings) {
|
|
71
|
+
this.settings = settings;
|
|
72
|
+
this.budget = new work_js_1.OperationBudget(settings.maxOperations);
|
|
73
|
+
this.reporter = new record_js_1.Reporter(settings.name, settings.onEvent);
|
|
74
|
+
this.budget.whenReleased((operation) => {
|
|
75
|
+
if (operation.settledLate) {
|
|
76
|
+
this.reporter.report({
|
|
77
|
+
kind: 'settled-late',
|
|
78
|
+
work: operation.phase,
|
|
79
|
+
durationMs: Math.max(0, this.now() - operation.admittedAt),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
this.retryDeferred();
|
|
83
|
+
this.notifySettled();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
now() {
|
|
87
|
+
return this.settings.now();
|
|
88
|
+
}
|
|
89
|
+
get signal() {
|
|
90
|
+
return this.lifetime.signal;
|
|
91
|
+
}
|
|
92
|
+
// ─── Admission ─────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* The lazy sweep, run at the start of every interaction: leases past
|
|
95
|
+
* `maxLeaseMs` revoked, idle clients ended, deferred endings retried — all of
|
|
96
|
+
* it before a new operation is admitted.
|
|
97
|
+
*/
|
|
98
|
+
interact() {
|
|
99
|
+
this.revokeExpiredLeases();
|
|
100
|
+
this.sweepIdle();
|
|
101
|
+
this.retryDeferred();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Admit one operation, or refuse at once: `closed`, or `PoolFullError`
|
|
105
|
+
* (`limit: 'operations'`) — before anything is sent.
|
|
106
|
+
*/
|
|
107
|
+
admit(kind, phase, callerSignal) {
|
|
108
|
+
if (this.closed)
|
|
109
|
+
throw this.refused('closed');
|
|
110
|
+
const operation = this.budget.admit(kind, phase, [this.lifetime.signal, callerSignal], this.now());
|
|
111
|
+
if (operation === undefined)
|
|
112
|
+
throw this.full('operations');
|
|
113
|
+
return operation;
|
|
114
|
+
}
|
|
115
|
+
refused(reason, cause) {
|
|
116
|
+
return new errors_js_1.McpPoolRefusedError(reason, this.settings.name, cause === undefined ? {} : { cause });
|
|
117
|
+
}
|
|
118
|
+
full(limit) {
|
|
119
|
+
const max = limit === 'pool'
|
|
120
|
+
? this.settings.maxClients
|
|
121
|
+
: limit === 'principal'
|
|
122
|
+
? this.settings.maxClientsPerPrincipal
|
|
123
|
+
: limit === 'ending'
|
|
124
|
+
? this.settings.maxEnding
|
|
125
|
+
: this.settings.maxOperations;
|
|
126
|
+
const busy = this.clients.openClients().filter((c) => c.busy).length;
|
|
127
|
+
return new errors_js_1.PoolFullError(this.settings.name, limit, max, busy, this.clients.endingCount, this.budget.count);
|
|
128
|
+
}
|
|
129
|
+
// ─── Clients and leases ────────────────────────────────────────────
|
|
130
|
+
/**
|
|
131
|
+
* A library lease on `key`'s client for `operation`, opening one when the key
|
|
132
|
+
* has none (inside the operation's slot). `plan` is the plan resolved for a
|
|
133
|
+
* new client; a key that already has one keeps its own. The lease is
|
|
134
|
+
* released when the operation SETTLES.
|
|
135
|
+
*/
|
|
136
|
+
async leaseClient(key, operation, facts, plan) {
|
|
137
|
+
facts.keyHash = key.keyHash;
|
|
138
|
+
for (let round = 0; round < 3; round += 1) {
|
|
139
|
+
const current = this.clients.currentFor(key.key);
|
|
140
|
+
if (current === undefined) {
|
|
141
|
+
const opened = this.openFor(key, operation, facts, plan);
|
|
142
|
+
const leased = await this.leaseOnceOpen(opened, operation, facts);
|
|
143
|
+
if (leased !== undefined)
|
|
144
|
+
return leased;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (current.abandoned)
|
|
148
|
+
throw this.refused('init-timeout');
|
|
149
|
+
if (current.state === 'open') {
|
|
150
|
+
if (facts.client === 'refused')
|
|
151
|
+
facts.client = 'reused';
|
|
152
|
+
return this.takeLease(current, operation, facts);
|
|
153
|
+
}
|
|
154
|
+
// Opening: join it, inside this operation's own slot.
|
|
155
|
+
facts.client = 'joined';
|
|
156
|
+
facts.clientId = current.clientId;
|
|
157
|
+
facts.generation = current.generation;
|
|
158
|
+
const leased = await this.leaseOnceOpen(current, operation, facts);
|
|
159
|
+
if (leased !== undefined)
|
|
160
|
+
return leased;
|
|
161
|
+
}
|
|
162
|
+
throw this.refused('init-failed');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Wait for `client`'s open inside `operation`, holding a reservation on it
|
|
166
|
+
* all the while — between the open settling and this lease the client is
|
|
167
|
+
* busy, so nobody can evict, idle-end or end it — then check it again and
|
|
168
|
+
* lease it in the same step, with no wait between. `undefined` when it no
|
|
169
|
+
* longer serves: the caller's next round finds the key's next client.
|
|
170
|
+
*/
|
|
171
|
+
async leaseOnceOpen(client, operation, facts) {
|
|
172
|
+
client.reserved += 1;
|
|
173
|
+
try {
|
|
174
|
+
await this.waitOpen(client, operation);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
client.reserved -= 1;
|
|
178
|
+
this.becameIdle(client);
|
|
179
|
+
// The pool closed while it waited: that is the refusal, whatever the wait threw.
|
|
180
|
+
throw this.closed ? this.refused('closed') : error;
|
|
181
|
+
}
|
|
182
|
+
client.reserved -= 1;
|
|
183
|
+
if (!client.serving) {
|
|
184
|
+
this.becameIdle(client);
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
return this.takeLease(client, operation, facts);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
this.becameIdle(client);
|
|
192
|
+
throw this.closed ? this.refused('closed') : error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** `client` may have just become idle: a deferred ending may run, and a waiter for idleness may go on. */
|
|
196
|
+
becameIdle(client) {
|
|
197
|
+
if (client.busy)
|
|
198
|
+
return;
|
|
199
|
+
this.retryDeferred();
|
|
200
|
+
this.notifyIdle();
|
|
201
|
+
}
|
|
202
|
+
takeLease(client, operation, facts) {
|
|
203
|
+
// The caller detached while it waited, or before it was admitted: nothing
|
|
204
|
+
// more is done for it. The cached path reaches here with no wait at all,
|
|
205
|
+
// so this is its only check.
|
|
206
|
+
operation.check();
|
|
207
|
+
client.leases += 1;
|
|
208
|
+
client.usedAt = this.now();
|
|
209
|
+
// Used again: an idle end or an eviction waiting for slots no longer applies.
|
|
210
|
+
if (client.deferredEnd === 'idle' || client.deferredEnd === 'evicted') {
|
|
211
|
+
client.deferredEnd = undefined;
|
|
212
|
+
}
|
|
213
|
+
facts.clientId = client.clientId;
|
|
214
|
+
facts.generation = client.generation;
|
|
215
|
+
if (client.initMs !== undefined && (facts.client === 'opened' || facts.client === 'joined')) {
|
|
216
|
+
facts.initMs = client.initMs;
|
|
217
|
+
}
|
|
218
|
+
operation.onSettled(() => this.releaseLease(client));
|
|
219
|
+
return client;
|
|
220
|
+
}
|
|
221
|
+
/** One lease ended: the client may now be idle, and a deferred ending may run. */
|
|
222
|
+
releaseLease(client) {
|
|
223
|
+
client.leases -= 1;
|
|
224
|
+
client.usedAt = this.now();
|
|
225
|
+
if (!client.busy && client.deferredEnd !== undefined) {
|
|
226
|
+
this.endClient(client, client.deferredEnd);
|
|
227
|
+
}
|
|
228
|
+
this.notifyIdle();
|
|
229
|
+
}
|
|
230
|
+
notifyIdle() {
|
|
231
|
+
for (const check of [...this.idleWaiters])
|
|
232
|
+
check();
|
|
233
|
+
}
|
|
234
|
+
/** An app lease: a hold on a client for as long as the app keeps it, at most `maxLeaseMs`. */
|
|
235
|
+
holdLease(client) {
|
|
236
|
+
client.leases += 1;
|
|
237
|
+
client.usedAt = this.now();
|
|
238
|
+
if (client.deferredEnd === 'idle' || client.deferredEnd === 'evicted') {
|
|
239
|
+
client.deferredEnd = undefined;
|
|
240
|
+
}
|
|
241
|
+
const lease = { client, takenAt: this.now(), revoked: false, released: false };
|
|
242
|
+
this.leases.add(lease);
|
|
243
|
+
return lease;
|
|
244
|
+
}
|
|
245
|
+
/** End an app lease — at most once, whether released or revoked. */
|
|
246
|
+
endLease(lease) {
|
|
247
|
+
if (!this.leases.delete(lease))
|
|
248
|
+
return;
|
|
249
|
+
this.releaseLease(lease.client);
|
|
250
|
+
}
|
|
251
|
+
revokeExpiredLeases() {
|
|
252
|
+
const now = this.now();
|
|
253
|
+
for (const lease of [...this.leases]) {
|
|
254
|
+
if (now - lease.takenAt < this.settings.maxLeaseMs)
|
|
255
|
+
continue;
|
|
256
|
+
lease.revoked = true;
|
|
257
|
+
this.endLease(lease);
|
|
258
|
+
this.reporter.report({
|
|
259
|
+
kind: 'lease-revoked',
|
|
260
|
+
clientId: lease.client.clientId,
|
|
261
|
+
keyHash: lease.client.key.keyHash,
|
|
262
|
+
durationMs: Math.max(0, now - lease.takenAt),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Every app lease is revoked: the pool is closing. */
|
|
267
|
+
revokeAllLeases() {
|
|
268
|
+
for (const lease of [...this.leases]) {
|
|
269
|
+
lease.revoked = true;
|
|
270
|
+
this.endLease(lease);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// ─── The open ──────────────────────────────────────────────────────
|
|
274
|
+
/**
|
|
275
|
+
* Open a client for `key`: make room, resolve its plan, admit it to the open
|
|
276
|
+
* set, and start the one open. The operation that started it tracks the open's
|
|
277
|
+
* settlement, so its charge stays until the open's work has settled.
|
|
278
|
+
*/
|
|
279
|
+
openFor(key, operation, facts, resolved) {
|
|
280
|
+
this.makeRoom(key);
|
|
281
|
+
let plan;
|
|
282
|
+
try {
|
|
283
|
+
plan = resolved ?? (0, plan_js_1.resolvePlan)(this.settings, key);
|
|
284
|
+
}
|
|
285
|
+
catch (cause) {
|
|
286
|
+
throw this.refused('init-failed', cause);
|
|
287
|
+
}
|
|
288
|
+
const { generation, previous } = this.clients.nextIncarnation(key.key);
|
|
289
|
+
const client = new pooledClient_js_1.PooledClient(key, plan, generation, this.now());
|
|
290
|
+
this.clients.admit(client);
|
|
291
|
+
facts.client = 'opened';
|
|
292
|
+
facts.clientId = client.clientId;
|
|
293
|
+
facts.generation = generation;
|
|
294
|
+
if (previous !== undefined)
|
|
295
|
+
facts.previous = previous;
|
|
296
|
+
const work = this.startOpen(client);
|
|
297
|
+
operation.phase = 'open';
|
|
298
|
+
operation.track(work.whenSettled());
|
|
299
|
+
return client;
|
|
300
|
+
}
|
|
301
|
+
/** The one open of `client`, under the pool's own signal and `initTimeoutMs`. */
|
|
302
|
+
startOpen(client) {
|
|
303
|
+
const work = new work_js_1.Work('open', [this.lifetime.signal]);
|
|
304
|
+
const startedAt = this.now();
|
|
305
|
+
let resolveReady;
|
|
306
|
+
let rejectReady;
|
|
307
|
+
const ready = new Promise((resolve, reject) => {
|
|
308
|
+
resolveReady = resolve;
|
|
309
|
+
rejectReady = reject;
|
|
310
|
+
});
|
|
311
|
+
ready.catch(() => undefined);
|
|
312
|
+
client.opening = { ready, work, startedAt };
|
|
313
|
+
client.phase = work;
|
|
314
|
+
let timedOut = false;
|
|
315
|
+
let opened;
|
|
316
|
+
let halfOpen;
|
|
317
|
+
const timer = setTimeout(() => {
|
|
318
|
+
timedOut = true;
|
|
319
|
+
client.abandoned = true;
|
|
320
|
+
work.abort(initTimeout());
|
|
321
|
+
rejectReady(this.refused('init-timeout'));
|
|
322
|
+
}, this.settings.initTimeoutMs);
|
|
323
|
+
const seam = this.seamFor(client, work.signal);
|
|
324
|
+
let pending;
|
|
325
|
+
try {
|
|
326
|
+
pending = (this.settings.open ?? mcpClient_js_1.openPooledClient)(this.clientOptions(client), seam);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
pending = Promise.reject(error);
|
|
330
|
+
}
|
|
331
|
+
work.track(pending);
|
|
332
|
+
pending.then((result) => {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
opened = result;
|
|
335
|
+
client.client = result.client;
|
|
336
|
+
client.port = result.port;
|
|
337
|
+
if (!timedOut && !this.closed) {
|
|
338
|
+
client.state = 'open';
|
|
339
|
+
client.initMs = Math.max(0, this.now() - startedAt);
|
|
340
|
+
client.opening = undefined;
|
|
341
|
+
if (client.phase === work)
|
|
342
|
+
client.phase = undefined;
|
|
343
|
+
resolveReady();
|
|
344
|
+
this.notifyIdle();
|
|
345
|
+
}
|
|
346
|
+
else if (!timedOut) {
|
|
347
|
+
// The pool closed while it opened: nobody gets it; it is ended below.
|
|
348
|
+
timedOut = true;
|
|
349
|
+
rejectReady(this.refused('closed'));
|
|
350
|
+
}
|
|
351
|
+
work.finish();
|
|
352
|
+
}, (error) => {
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
halfOpen = (0, clientPort_js_1.halfOpenOf)(error);
|
|
355
|
+
if (!timedOut) {
|
|
356
|
+
rejectReady(this.closed ? this.refused('closed') : this.refused('init-failed', error));
|
|
357
|
+
}
|
|
358
|
+
work.finish();
|
|
359
|
+
});
|
|
360
|
+
// Once the open's own work has settled: an abandoned open that opened a
|
|
361
|
+
// session is ended under the ending rule; a failed one frees its slot.
|
|
362
|
+
// Either way the client has stopped opening — a wait for idle clients
|
|
363
|
+
// (`terminate`, `close`) is told once its new state is in place.
|
|
364
|
+
work.onSettled(() => {
|
|
365
|
+
if (!timedOut && opened !== undefined) {
|
|
366
|
+
this.notifyIdle();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
client.opening = undefined;
|
|
370
|
+
if (client.phase === work)
|
|
371
|
+
client.phase = undefined;
|
|
372
|
+
// A failed open whose server opened a session all the same has a session
|
|
373
|
+
// to end: so does an abandoned open that succeeded.
|
|
374
|
+
const session = opened?.port ?? (halfOpen?.hasSession() === true ? halfOpen : undefined);
|
|
375
|
+
if (session === undefined) {
|
|
376
|
+
this.dropFailedOpen(client);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
if (opened === undefined)
|
|
380
|
+
client.port = endingOnly(session);
|
|
381
|
+
client.state = 'open';
|
|
382
|
+
client.closedToNew = true;
|
|
383
|
+
this.clients.retire(client, 'terminated');
|
|
384
|
+
this.endClient(client, 'abandoned-open');
|
|
385
|
+
}
|
|
386
|
+
this.notifyIdle();
|
|
387
|
+
});
|
|
388
|
+
return work;
|
|
389
|
+
}
|
|
390
|
+
dropFailedOpen(client) {
|
|
391
|
+
client.opening = undefined;
|
|
392
|
+
client.state = 'ended';
|
|
393
|
+
client.phase = undefined;
|
|
394
|
+
this.clients.drop(client);
|
|
395
|
+
this.retryDeferred();
|
|
396
|
+
this.notifySettled();
|
|
397
|
+
}
|
|
398
|
+
/** Wait for `client`'s open inside `operation`: the caller's signal detaches the caller only. */
|
|
399
|
+
async waitOpen(client, operation) {
|
|
400
|
+
const opening = client.opening;
|
|
401
|
+
if (opening === undefined) {
|
|
402
|
+
if (client.state === 'open')
|
|
403
|
+
return;
|
|
404
|
+
throw this.refused(client.abandoned ? 'init-timeout' : 'init-failed');
|
|
405
|
+
}
|
|
406
|
+
await (0, abort_js_1.untilAborted)(opening.ready, operation.signal);
|
|
407
|
+
}
|
|
408
|
+
/** What `mcpClient` is opened with: the connection-shaping options, the pool's signal. */
|
|
409
|
+
clientOptions(client) {
|
|
410
|
+
return {
|
|
411
|
+
name: this.settings.name,
|
|
412
|
+
transport: client.plan.transport,
|
|
413
|
+
signal: this.lifetime.signal,
|
|
414
|
+
...this.settings.client,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** What only a pool asks of the client it opens (`clientPort.ts`). */
|
|
418
|
+
seamFor(client, connectSignal) {
|
|
419
|
+
return {
|
|
420
|
+
connectSignal,
|
|
421
|
+
wrapFetch: (inner, claim) => this.pooledFetch(client, inner, claim),
|
|
422
|
+
guardFor: (message) => this.guardOf(client, message),
|
|
423
|
+
onToolsChanged: () => this.toolsChangedOn(client),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* The guard for a message on `client` that carries none of its own: a tool
|
|
428
|
+
* call always brings its call's guard, so one without it is refused; any
|
|
429
|
+
* other request runs under the client's open, listing or ending — and with
|
|
430
|
+
* none under way, it is refused too. Nothing pooled is sent unchecked.
|
|
431
|
+
*/
|
|
432
|
+
guardOf(client, message) {
|
|
433
|
+
if (isToolCall(message))
|
|
434
|
+
return this.unmatched;
|
|
435
|
+
return client.phase ?? this.unmatched;
|
|
436
|
+
}
|
|
437
|
+
/** The guard of a request nothing covers: it refuses, so the request is never sent. */
|
|
438
|
+
unmatched = {
|
|
439
|
+
check: () => {
|
|
440
|
+
throw this.refused('request-unmatched');
|
|
441
|
+
},
|
|
442
|
+
track: () => undefined,
|
|
443
|
+
};
|
|
444
|
+
/**
|
|
445
|
+
* The pool's own fetch, outermost on every pooled transport: a request is
|
|
446
|
+
* checked against its guard (the pooled call's, else the client's phase) and
|
|
447
|
+
* counted under it; the guard's signal is linked in; and the answer is read
|
|
448
|
+
* for the two pieces of evidence the pool classifies on — a 404 the MCP server
|
|
449
|
+
* itself sent to a request that carried its session id, and a handle this
|
|
450
|
+
* process lost. The client's own standalone stream (a GET), a notification and
|
|
451
|
+
* an answer to the server's own request are the client's, not an operation's:
|
|
452
|
+
* they pass untouched. Any other request with no guard is refused unsent.
|
|
453
|
+
*/
|
|
454
|
+
pooledFetch(client, inner, claim) {
|
|
455
|
+
return async (input, init) => {
|
|
456
|
+
// The client's own stream, a notification, an answer to the server —
|
|
457
|
+
// never refused, never cut by an operation; a cancellation is counted
|
|
458
|
+
// under its call at `send`.
|
|
459
|
+
if ((init?.method ?? 'GET').toUpperCase() === 'GET' || passesUnguarded(init?.body)) {
|
|
460
|
+
return inner(input, init);
|
|
461
|
+
}
|
|
462
|
+
const guard = claim(init?.body) ?? client.phase;
|
|
463
|
+
if (guard === undefined)
|
|
464
|
+
throw this.refused('request-unmatched');
|
|
465
|
+
guard.check();
|
|
466
|
+
const carriesSession = new Headers(init?.headers).has('mcp-session-id');
|
|
467
|
+
const signal = (0, abort_js_1.eitherSignal)(init?.signal ?? undefined, guard.signal);
|
|
468
|
+
const sent = inner(input, {
|
|
469
|
+
...(0, requestGuard_js_1.withGuard)(init, guard),
|
|
470
|
+
...(signal !== undefined && { signal }),
|
|
471
|
+
});
|
|
472
|
+
guard.track(sent);
|
|
473
|
+
try {
|
|
474
|
+
const response = await sent;
|
|
475
|
+
if (response.status === 404 && carriesSession && guard instanceof work_js_1.Work) {
|
|
476
|
+
if (await isServerAnswer(response))
|
|
477
|
+
guard.note('server-404');
|
|
478
|
+
}
|
|
479
|
+
return response;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
if ((0, toolSessions_js_1.isLocalSessionHandleLost)(error) && guard instanceof work_js_1.Work)
|
|
483
|
+
guard.note('handle-lost');
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// ─── Making room, and ending ───────────────────────────────────────
|
|
489
|
+
/**
|
|
490
|
+
* Make room for a new client of `key`: the principal's least recently used
|
|
491
|
+
* idle client, then the pool's, moves to the ending set — or `PoolFullError`
|
|
492
|
+
* at once, naming the budget. There is no queue.
|
|
493
|
+
*/
|
|
494
|
+
makeRoom(key) {
|
|
495
|
+
if (this.clients.principalCount(key.principal) >= this.settings.maxClientsPerPrincipal) {
|
|
496
|
+
const victim = this.clients.leastRecentlyUsedIdle((c) => c.key.principal === key.principal);
|
|
497
|
+
if (victim === undefined)
|
|
498
|
+
throw this.full('principal');
|
|
499
|
+
this.evict(victim);
|
|
500
|
+
}
|
|
501
|
+
if (this.clients.openCount >= this.settings.maxClients) {
|
|
502
|
+
const victim = this.clients.leastRecentlyUsedIdle(() => true);
|
|
503
|
+
if (victim === undefined)
|
|
504
|
+
throw this.full('pool');
|
|
505
|
+
this.evict(victim);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Move `victim` to the ending set to make room. When the slots cannot be
|
|
510
|
+
* reserved, the arrival is refused at once, and the victim stays in the open
|
|
511
|
+
* set, counted, reported deferred — ended once slots free.
|
|
512
|
+
*/
|
|
513
|
+
evict(victim) {
|
|
514
|
+
const reserved = this.reserveEnding();
|
|
515
|
+
if (typeof reserved === 'string') {
|
|
516
|
+
this.defer(victim, 'evicted');
|
|
517
|
+
throw this.full(reserved);
|
|
518
|
+
}
|
|
519
|
+
// A victim already waiting to end for a stronger reason ends for that one.
|
|
520
|
+
const waiting = victim.deferredEnd;
|
|
521
|
+
const reason = waiting !== undefined && waiting !== 'idle' ? waiting : 'evicted';
|
|
522
|
+
this.startEnding(victim, reason, reserved);
|
|
523
|
+
}
|
|
524
|
+
/** An ending slot and an operation slot, both, or which one is full. */
|
|
525
|
+
reserveEnding() {
|
|
526
|
+
if (this.clients.endingCount >= this.settings.maxEnding)
|
|
527
|
+
return 'ending';
|
|
528
|
+
return this.budget.admit('ending', 'ending', [], this.now()) ?? 'operations';
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* End `client` for `reason` — when it is idle and both slots can be reserved.
|
|
532
|
+
* Otherwise it stays in the open set, still counted, and is reported deferred
|
|
533
|
+
* (once); it is ended when its last lease ends or slots free.
|
|
534
|
+
*/
|
|
535
|
+
endClient(client, reason) {
|
|
536
|
+
if (client.state !== 'open' || client.busy || client.port === undefined) {
|
|
537
|
+
this.defer(client, reason);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const reserved = this.reserveEnding();
|
|
541
|
+
if (typeof reserved === 'string') {
|
|
542
|
+
this.defer(client, reason);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this.startEnding(client, reason, reserved);
|
|
546
|
+
}
|
|
547
|
+
defer(client, reason) {
|
|
548
|
+
const first = client.deferredEnd === undefined;
|
|
549
|
+
// A stronger reason replaces a weaker one: terminated, closed or expired
|
|
550
|
+
// outrank an idle end or an eviction.
|
|
551
|
+
if (first || client.deferredEnd === 'idle' || client.deferredEnd === 'evicted') {
|
|
552
|
+
client.deferredEnd = reason;
|
|
553
|
+
}
|
|
554
|
+
if (!first || client.state === 'ending' || client.state === 'ended')
|
|
555
|
+
return;
|
|
556
|
+
this.reporter.report({
|
|
557
|
+
kind: 'deferred',
|
|
558
|
+
clientId: client.clientId,
|
|
559
|
+
keyHash: client.key.keyHash,
|
|
560
|
+
reason,
|
|
561
|
+
durationMs: 0,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Move `client` into the ending set under `operation`, and end it through
|
|
566
|
+
* `terminate()` — or, for a lost handle, through its local close alone.
|
|
567
|
+
*/
|
|
568
|
+
startEnding(client, reason, operation) {
|
|
569
|
+
client.deferredEnd = undefined;
|
|
570
|
+
client.state = 'ending';
|
|
571
|
+
this.clients.moveToEnding(client, (0, clientSet_js_1.previousOf)(reason));
|
|
572
|
+
client.phase = operation;
|
|
573
|
+
const ending = client.lost === 'handle-lost'
|
|
574
|
+
? closeLocally(client)
|
|
575
|
+
: client.port.end();
|
|
576
|
+
client.endingOutcome = ending.outcome;
|
|
577
|
+
operation.track(ending.settled);
|
|
578
|
+
void ending.outcome.then((outcome) => {
|
|
579
|
+
this.noteEnded(client, reason, outcome);
|
|
580
|
+
operation.finish();
|
|
581
|
+
});
|
|
582
|
+
operation.onSettled(() => {
|
|
583
|
+
this.clients.ended(client);
|
|
584
|
+
client.state = 'ended';
|
|
585
|
+
client.phase = undefined;
|
|
586
|
+
// The ending slot is free only now: deferred endings may run, and a
|
|
587
|
+
// `settled()` waiting on the ending set may resolve.
|
|
588
|
+
this.retryDeferred();
|
|
589
|
+
this.notifySettled();
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
noteEnded(client, reason, outcome) {
|
|
593
|
+
if (!outcome.resolved)
|
|
594
|
+
this.unresolvedEndings += 1;
|
|
595
|
+
this.reporter.report({
|
|
596
|
+
kind: 'ended',
|
|
597
|
+
clientId: client.clientId,
|
|
598
|
+
keyHash: client.key.keyHash,
|
|
599
|
+
reason,
|
|
600
|
+
remote: outcome.remote,
|
|
601
|
+
resolved: outcome.resolved,
|
|
602
|
+
...(outcome.status !== undefined && { status: outcome.status }),
|
|
603
|
+
...(outcome.errorClass !== undefined && { errorClass: outcome.errorClass }),
|
|
604
|
+
durationMs: outcome.durationMs,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* A client whose handle this process lost: nothing can be sent for it, so it
|
|
609
|
+
* leaves its key at once — the next use opens the key's next client — and is
|
|
610
|
+
* ended under the ending rule like any other: its ending is its LOCAL close,
|
|
611
|
+
* charged an ending slot and an operation slot until that close settles, its
|
|
612
|
+
* remote outcome recorded unresolved. Busy, or with no slots free, it waits —
|
|
613
|
+
* still counted in the open set, reported deferred.
|
|
614
|
+
*/
|
|
615
|
+
lostHandle(client) {
|
|
616
|
+
if (client.lost !== undefined || client.state !== 'open')
|
|
617
|
+
return;
|
|
618
|
+
client.lost = 'handle-lost';
|
|
619
|
+
client.closedToNew = true;
|
|
620
|
+
this.clients.retire(client, 'handle-lost');
|
|
621
|
+
this.endClient(client, 'handle-lost');
|
|
622
|
+
}
|
|
623
|
+
/** A client whose session the server ended: no new lease; ended (through `terminate()`) when idle. */
|
|
624
|
+
expired(client) {
|
|
625
|
+
if (client.lost !== undefined)
|
|
626
|
+
return;
|
|
627
|
+
client.lost = 'expired';
|
|
628
|
+
client.closedToNew = true;
|
|
629
|
+
this.clients.retire(client, 'expired');
|
|
630
|
+
this.endClient(client, 'expired');
|
|
631
|
+
}
|
|
632
|
+
sweepIdle() {
|
|
633
|
+
const cutoff = this.now() - this.settings.idleMs;
|
|
634
|
+
for (const client of this.clients.openClients()) {
|
|
635
|
+
if (client.state !== 'open' || client.busy || client.deferredEnd !== undefined)
|
|
636
|
+
continue;
|
|
637
|
+
// Retired only once its ending starts (`startEnding`): while it waits
|
|
638
|
+
// for slots, its own conversation may still use it.
|
|
639
|
+
if (client.usedAt <= cutoff)
|
|
640
|
+
this.endClient(client, 'idle');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** Retry every deferred ending whose client is now idle. */
|
|
644
|
+
retryDeferred() {
|
|
645
|
+
for (const client of this.clients.openClients()) {
|
|
646
|
+
if (client.deferredEnd === undefined || client.busy || client.state !== 'open')
|
|
647
|
+
continue;
|
|
648
|
+
this.endClient(client, client.deferredEnd);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// ─── Lists ─────────────────────────────────────────────────────────
|
|
652
|
+
/** Where `client`'s definitions are served from: its shared entry, or its own list. */
|
|
653
|
+
servedListOf(client) {
|
|
654
|
+
const { context, shared } = client.definitions;
|
|
655
|
+
if (shared) {
|
|
656
|
+
return { list: this.shared.entryFor(context, client.key.tenant).list, shared: true, context };
|
|
657
|
+
}
|
|
658
|
+
return { list: client.list, shared: false, context };
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* The definitions context `client` serves under NOW: the app's `share` asked
|
|
662
|
+
* again over the client's own configuration. A changed answer moves the
|
|
663
|
+
* client to the new context and stops serving its own list, so every tool
|
|
664
|
+
* bound under the old one is refused before it is sent.
|
|
665
|
+
*
|
|
666
|
+
* @throws `binding-changed`, `share`'s error as its cause, when the answer
|
|
667
|
+
* cannot be had — never the old context
|
|
668
|
+
*/
|
|
669
|
+
refreshContext(client) {
|
|
670
|
+
if (this.settings.share === undefined)
|
|
671
|
+
return client.definitions;
|
|
672
|
+
let now;
|
|
673
|
+
try {
|
|
674
|
+
now = (0, plan_js_1.contextFor)(this.settings, client.key, client.plan.configHash);
|
|
675
|
+
}
|
|
676
|
+
catch (cause) {
|
|
677
|
+
throw this.refused('binding-changed', cause);
|
|
678
|
+
}
|
|
679
|
+
if (now.context !== client.definitions.context) {
|
|
680
|
+
client.definitions = now;
|
|
681
|
+
client.list.invalidate();
|
|
682
|
+
}
|
|
683
|
+
return now;
|
|
684
|
+
}
|
|
685
|
+
/** Does `context` still name the definitions context `client` serves under? False when it cannot be had. */
|
|
686
|
+
contextHolds(client, context) {
|
|
687
|
+
try {
|
|
688
|
+
return this.refreshContext(client).context === context;
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* The list `client` serves under NOW, listed again on it when it cannot be
|
|
696
|
+
* served as it stands — one listing at a time per list; a second caller joins
|
|
697
|
+
* it inside its own slot. Every attempt asks the app's names again
|
|
698
|
+
* (`refreshContext`), so a listing that a moved context discarded or refused
|
|
699
|
+
* at its send is listed once more under the context of now, never written
|
|
700
|
+
* into the one it began under. A listing raced by an invalidation is
|
|
701
|
+
* discarded too. Refused (`stale-tools`) when three attempts serve nothing.
|
|
702
|
+
* What it returns was checked against the context with no wait since.
|
|
703
|
+
*/
|
|
704
|
+
async ensureListed(client, operation) {
|
|
705
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
706
|
+
const served = this.servableNow(client);
|
|
707
|
+
if (served.ready !== undefined)
|
|
708
|
+
return served.ready;
|
|
709
|
+
const { list } = served.wanted;
|
|
710
|
+
if (list.listing === undefined)
|
|
711
|
+
this.startListing(client, served.wanted, operation);
|
|
712
|
+
operation.phase = 'listing';
|
|
713
|
+
try {
|
|
714
|
+
await (0, abort_js_1.untilAborted)(list.listing, operation.signal);
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
operation.check();
|
|
718
|
+
// Refused at its send because the context moved: asked again next round.
|
|
719
|
+
if (!isContextMoved(error))
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
operation.check();
|
|
723
|
+
}
|
|
724
|
+
const served = this.servableNow(client);
|
|
725
|
+
if (served.ready !== undefined)
|
|
726
|
+
return served.ready;
|
|
727
|
+
throw this.refused('stale-tools');
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* What `client` can serve under the context of now — asked of the app's names
|
|
731
|
+
* here — or, when nothing can, the list it should list. Too long to share:
|
|
732
|
+
* the conversation's own client keeps that list, under the same context.
|
|
733
|
+
*/
|
|
734
|
+
servableNow(client) {
|
|
735
|
+
this.refreshContext(client);
|
|
736
|
+
const wanted = this.servedListOf(client);
|
|
737
|
+
const now = this.now();
|
|
738
|
+
const ttl = this.settings.definitionTtlMs;
|
|
739
|
+
if (wanted.list.servable(now, ttl))
|
|
740
|
+
return { ready: wanted, wanted };
|
|
741
|
+
// A context change invalidates the client's own list, so a servable one
|
|
742
|
+
// was written under the context of now.
|
|
743
|
+
if (wanted.shared && client.list.servable(now, ttl)) {
|
|
744
|
+
return { ready: { list: client.list, shared: false, context: wanted.context }, wanted };
|
|
745
|
+
}
|
|
746
|
+
return { wanted };
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* One listing on `client` for `served`'s context. Its requests are sent only
|
|
750
|
+
* while that context holds — asked at every boundary they cross — and its
|
|
751
|
+
* answer is written only if the context still holds when it arrives and the
|
|
752
|
+
* list's generation held: an answer the app's names moved under belongs to
|
|
753
|
+
* no context, and is dropped.
|
|
754
|
+
*/
|
|
755
|
+
startListing(client, served, operation) {
|
|
756
|
+
const { list } = served;
|
|
757
|
+
const began = list.generation;
|
|
758
|
+
const beganOwn = client.list.generation;
|
|
759
|
+
const work = new work_js_1.Work('listing', [this.lifetime.signal]);
|
|
760
|
+
work.guardWith(() => {
|
|
761
|
+
if (!this.contextHolds(client, served.context))
|
|
762
|
+
throw this.refused('binding-changed');
|
|
763
|
+
});
|
|
764
|
+
operation.track(work.whenSettled());
|
|
765
|
+
const port = client.port;
|
|
766
|
+
if (port === undefined)
|
|
767
|
+
throw this.refused('init-failed');
|
|
768
|
+
client.phase = work;
|
|
769
|
+
const pending = port.listing(work.signal);
|
|
770
|
+
work.track(pending);
|
|
771
|
+
list.listing = pending
|
|
772
|
+
.then((listing) => {
|
|
773
|
+
// Asked again before anything is kept: listed under one context,
|
|
774
|
+
// answered under another, it is no one's.
|
|
775
|
+
if (!this.contextHolds(client, served.context))
|
|
776
|
+
return;
|
|
777
|
+
const definitions = listing.listed
|
|
778
|
+
.map((listed) => (0, definitions_js_1.definitionOf)(listed))
|
|
779
|
+
.filter((d) => d !== undefined);
|
|
780
|
+
if (served.shared && definitions.length > options_js_1.MAX_SHARED_TOOLS) {
|
|
781
|
+
// Used by its own client, never shared.
|
|
782
|
+
client.list.write(beganOwn, definitions, listing.tools, this.now());
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
list.write(began, definitions, served.shared ? undefined : listing.tools, this.now());
|
|
786
|
+
})
|
|
787
|
+
.catch((error) => {
|
|
788
|
+
this.classifyFailure(client, work, error);
|
|
789
|
+
throw error;
|
|
790
|
+
})
|
|
791
|
+
.finally(() => {
|
|
792
|
+
list.listing = undefined;
|
|
793
|
+
if (client.phase === work)
|
|
794
|
+
client.phase = undefined;
|
|
795
|
+
work.finish();
|
|
796
|
+
});
|
|
797
|
+
list.listing.catch(() => undefined);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* A request failed: when the pool's fetch read the server's own 404 for the
|
|
801
|
+
* session, or this process lost the handle, the client leaves its key — the
|
|
802
|
+
* next use opens the next client under the SAME key — and the error says so.
|
|
803
|
+
*/
|
|
804
|
+
classifyFailure(client, work, error) {
|
|
805
|
+
if ((0, toolSessions_js_1.isLocalSessionHandleLost)(error) || work.evidence === 'handle-lost') {
|
|
806
|
+
this.lostHandle(client);
|
|
807
|
+
return 'handle-lost';
|
|
808
|
+
}
|
|
809
|
+
if (work.evidence === 'server-404') {
|
|
810
|
+
this.expired(client);
|
|
811
|
+
return 'expired';
|
|
812
|
+
}
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
// ─── Invalidation ──────────────────────────────────────────────────
|
|
816
|
+
/** The server said a client's tool list changed: that client's list and its shared entry. */
|
|
817
|
+
toolsChangedOn(client) {
|
|
818
|
+
client.list.invalidate();
|
|
819
|
+
const { context, shared } = client.definitions;
|
|
820
|
+
if (shared)
|
|
821
|
+
this.shared.peek(context)?.list.invalidate();
|
|
822
|
+
}
|
|
823
|
+
/** The app's permission signal: every list in the scope. */
|
|
824
|
+
permissionsChanged(scope) {
|
|
825
|
+
const matches = (0, identityKey_js_1.scopeMatcher)({
|
|
826
|
+
...(scope.tenant !== undefined && { tenant: scope.tenant }),
|
|
827
|
+
...(scope.principal !== undefined && { principal: scope.principal }),
|
|
828
|
+
});
|
|
829
|
+
for (const client of this.clients.openClients()) {
|
|
830
|
+
if (matches(client.key))
|
|
831
|
+
client.list.invalidate();
|
|
832
|
+
}
|
|
833
|
+
// A shared entry is used across the tenant's principals: any of them changing invalidates it.
|
|
834
|
+
const tenant = (0, encode_js_1.encodeIdentityField)(scope.tenant);
|
|
835
|
+
this.shared.invalidate((entry) => entry.tenant === tenant);
|
|
836
|
+
}
|
|
837
|
+
/** The server changed: every list. */
|
|
838
|
+
toolsChanged() {
|
|
839
|
+
for (const client of this.clients.openClients())
|
|
840
|
+
client.list.invalidate();
|
|
841
|
+
this.shared.invalidate(() => true);
|
|
842
|
+
}
|
|
843
|
+
// ─── Terminate, close, settle ──────────────────────────────────────
|
|
844
|
+
/**
|
|
845
|
+
* Stop new leases on every client `scope` names, at once; wait for their
|
|
846
|
+
* in-flight work up to `waitMs`; then end each one whose slots can be
|
|
847
|
+
* reserved. The rest are reported deferred and ended when they can be.
|
|
848
|
+
*/
|
|
849
|
+
async terminate(matches, reason, waitMs) {
|
|
850
|
+
const named = this.clients.openClients().filter((c) => matches(c.key));
|
|
851
|
+
for (const client of named) {
|
|
852
|
+
client.closedToNew = true;
|
|
853
|
+
this.clients.retire(client, (0, clientSet_js_1.previousOf)(reason) ?? 'terminated');
|
|
854
|
+
}
|
|
855
|
+
await this.waitIdle(named, waitMs);
|
|
856
|
+
return this.endNow(named, reason);
|
|
857
|
+
}
|
|
858
|
+
/** End each of `clients` whose slots can be reserved; the outcomes of those that started. */
|
|
859
|
+
endNow(clients, reason) {
|
|
860
|
+
const outcomes = [];
|
|
861
|
+
for (const client of clients) {
|
|
862
|
+
if (client.state !== 'open')
|
|
863
|
+
continue;
|
|
864
|
+
this.endClient(client, reason);
|
|
865
|
+
if (client.state === 'ending' && client.endingOutcome !== undefined) {
|
|
866
|
+
outcomes.push(client.endingOutcome);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return Promise.all(outcomes);
|
|
870
|
+
}
|
|
871
|
+
/** Wait until every client in `clients` is idle, or `waitMs` passed. */
|
|
872
|
+
waitIdle(clients, waitMs) {
|
|
873
|
+
if (!clients.some((c) => c.busy))
|
|
874
|
+
return Promise.resolve();
|
|
875
|
+
return new Promise((resolve) => {
|
|
876
|
+
const done = () => {
|
|
877
|
+
clearTimeout(timer);
|
|
878
|
+
this.idleWaiters.delete(check);
|
|
879
|
+
resolve();
|
|
880
|
+
};
|
|
881
|
+
const check = () => {
|
|
882
|
+
if (!clients.some((c) => c.busy))
|
|
883
|
+
done();
|
|
884
|
+
};
|
|
885
|
+
const timer = setTimeout(done, waitMs);
|
|
886
|
+
timer.unref?.();
|
|
887
|
+
this.idleWaiters.add(check);
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Close: refuse new work, revoke app leases, abort in-flight work through the
|
|
892
|
+
* pool's own signal, then end every client as slots can be reserved — at most
|
|
893
|
+
* `maxEnding` at a time — in rounds, each bounded by `waitMs` (and each
|
|
894
|
+
* ending's answer by its own deadline). What could not be ended yet is
|
|
895
|
+
* reported deferred and ended when slots free; work still unsettled stays
|
|
896
|
+
* charged until it settles.
|
|
897
|
+
*/
|
|
898
|
+
async closeAll(waitMs) {
|
|
899
|
+
this.shutDown();
|
|
900
|
+
const all = this.clients.openClients();
|
|
901
|
+
for (const client of all) {
|
|
902
|
+
client.closedToNew = true;
|
|
903
|
+
this.clients.retire(client, 'terminated');
|
|
904
|
+
}
|
|
905
|
+
await this.waitIdle(all, waitMs);
|
|
906
|
+
const outcomes = [];
|
|
907
|
+
for (;;) {
|
|
908
|
+
const started = await this.endNow(all, 'closed');
|
|
909
|
+
if (started.length === 0)
|
|
910
|
+
break;
|
|
911
|
+
outcomes.push(...started);
|
|
912
|
+
if (!all.some((c) => c.state === 'open'))
|
|
913
|
+
break;
|
|
914
|
+
await this.waitSlots(waitMs);
|
|
915
|
+
}
|
|
916
|
+
for (const client of all) {
|
|
917
|
+
if (client.state === 'open' || client.state === 'opening')
|
|
918
|
+
this.defer(client, 'closed');
|
|
919
|
+
}
|
|
920
|
+
return outcomes;
|
|
921
|
+
}
|
|
922
|
+
/** Wait until an ending slot and an operation slot are free, or `waitMs` passed. */
|
|
923
|
+
waitSlots(waitMs) {
|
|
924
|
+
const free = () => this.clients.endingCount < this.settings.maxEnding && this.budget.hasRoom;
|
|
925
|
+
if (free())
|
|
926
|
+
return Promise.resolve();
|
|
927
|
+
return new Promise((resolve) => {
|
|
928
|
+
const done = () => {
|
|
929
|
+
clearTimeout(timer);
|
|
930
|
+
unsubscribe();
|
|
931
|
+
resolve();
|
|
932
|
+
};
|
|
933
|
+
const timer = setTimeout(done, waitMs);
|
|
934
|
+
timer.unref?.();
|
|
935
|
+
const unsubscribe = this.budget.whenReleased(() => {
|
|
936
|
+
if (free())
|
|
937
|
+
done();
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
/** Refuse new work, revoke app leases, abort in-flight work through the pool's signal. */
|
|
942
|
+
shutDown() {
|
|
943
|
+
this.closed = true;
|
|
944
|
+
this.revokeAllLeases();
|
|
945
|
+
this.lifetime.abort(poolClosed());
|
|
946
|
+
}
|
|
947
|
+
/** Resolve once every charged operation and every ending has settled. */
|
|
948
|
+
settled() {
|
|
949
|
+
if (this.quiet())
|
|
950
|
+
return Promise.resolve();
|
|
951
|
+
return new Promise((resolve) => this.settleWaiters.push(resolve));
|
|
952
|
+
}
|
|
953
|
+
quiet() {
|
|
954
|
+
return (this.budget.count === 0 &&
|
|
955
|
+
this.clients.endingCount === 0 &&
|
|
956
|
+
!this.clients.openClients().some((c) => c.state === 'opening'));
|
|
957
|
+
}
|
|
958
|
+
notifySettled() {
|
|
959
|
+
if (!this.quiet())
|
|
960
|
+
return;
|
|
961
|
+
for (const resolve of this.settleWaiters.splice(0))
|
|
962
|
+
resolve();
|
|
963
|
+
}
|
|
964
|
+
stats() {
|
|
965
|
+
const open = this.clients.openClients();
|
|
966
|
+
const busy = open.filter((c) => c.busy).length;
|
|
967
|
+
return {
|
|
968
|
+
open: open.length,
|
|
969
|
+
busy,
|
|
970
|
+
idle: open.length - busy,
|
|
971
|
+
opening: open.filter((c) => c.state === 'opening').length,
|
|
972
|
+
ending: this.clients.endingCount,
|
|
973
|
+
operations: this.budget.count,
|
|
974
|
+
entries: this.shared.size,
|
|
975
|
+
unsettled: this.budget.unsettled(),
|
|
976
|
+
unresolvedEndings: this.unresolvedEndings,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
/** The bound shape of every definition in `served`, for a client of `key`. */
|
|
980
|
+
definitionsOf(served) {
|
|
981
|
+
return served.list.all();
|
|
982
|
+
}
|
|
983
|
+
/** The tools a list's client-own entries carry, by name (per-client lists only). */
|
|
984
|
+
ownTool(served, name) {
|
|
985
|
+
return served.shared ? undefined : served.list.tool(name);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
exports.Pool = Pool;
|
|
989
|
+
/** A port that can only end: what a half-opened session leaves the pool. */
|
|
990
|
+
function endingOnly(halfOpen) {
|
|
991
|
+
return {
|
|
992
|
+
listing: () => Promise.reject(new Error('This client never opened.')),
|
|
993
|
+
bind: () => {
|
|
994
|
+
throw new Error('This client never opened.');
|
|
995
|
+
},
|
|
996
|
+
end: () => halfOpen.end(),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
/** The reason an open is aborted at its deadline — a fixed sentence. */
|
|
1000
|
+
function initTimeout() {
|
|
1001
|
+
return Object.assign(new Error('The pooled client did not open within its deadline.'), {
|
|
1002
|
+
name: 'TimeoutError',
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
/** The reason in-flight work is aborted when the pool closes — a fixed sentence. */
|
|
1006
|
+
function poolClosed() {
|
|
1007
|
+
return Object.assign(new Error('The MCP client pool was closed.'), { name: 'AbortError' });
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* A body the pool's fetch passes with no guard: a JSON-RPC notification (a
|
|
1011
|
+
* method, no id) or an answer to the server's own request (an id, a result or
|
|
1012
|
+
* an error, no method). Never refused, never counted here. Anything else — a
|
|
1013
|
+
* request, a batch, a body that is not JSON-RPC — needs a guard.
|
|
1014
|
+
*/
|
|
1015
|
+
function passesUnguarded(body) {
|
|
1016
|
+
if (typeof body !== 'string')
|
|
1017
|
+
return false;
|
|
1018
|
+
let message;
|
|
1019
|
+
try {
|
|
1020
|
+
message = JSON.parse(body);
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
if (message === null || typeof message !== 'object' || Array.isArray(message))
|
|
1026
|
+
return false;
|
|
1027
|
+
const fields = message;
|
|
1028
|
+
if (typeof fields.method === 'string')
|
|
1029
|
+
return fields.id === undefined;
|
|
1030
|
+
return fields.method === undefined && ('result' in fields || 'error' in fields);
|
|
1031
|
+
}
|
|
1032
|
+
/** A listing refused at its send because the app's names moved: listed again under the new context. */
|
|
1033
|
+
function isContextMoved(error) {
|
|
1034
|
+
return error instanceof errors_js_1.McpPoolRefusedError && error.reason === 'binding-changed';
|
|
1035
|
+
}
|
|
1036
|
+
/** A `tools/call` request: a pooled one always brings its own call's guard. */
|
|
1037
|
+
function isToolCall(message) {
|
|
1038
|
+
return (message !== null &&
|
|
1039
|
+
typeof message === 'object' &&
|
|
1040
|
+
message.method === 'tools/call');
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* A lost handle's ending: nothing can be sent for it, so the remote outcome is
|
|
1044
|
+
* known at once — `handle-lost`, unresolved — and the work is the local close
|
|
1045
|
+
* alone, which the ending stays charged for until it settles.
|
|
1046
|
+
*/
|
|
1047
|
+
function closeLocally(client) {
|
|
1048
|
+
const target = client.client;
|
|
1049
|
+
const settled = target === undefined
|
|
1050
|
+
? Promise.resolve()
|
|
1051
|
+
: Promise.resolve()
|
|
1052
|
+
.then(() => target.close())
|
|
1053
|
+
.then(() => undefined, () => undefined);
|
|
1054
|
+
return {
|
|
1055
|
+
outcome: Promise.resolve({ remote: 'handle-lost', resolved: false, durationMs: 0 }),
|
|
1056
|
+
settled,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Is a 404 the MCP server's own answer — a JSON-RPC error response — rather than
|
|
1061
|
+
* an intermediary's (a platform's "not found" for a missing endpoint, a proxy's
|
|
1062
|
+
* page)? Read from a copy of the body, which the SDK reads too.
|
|
1063
|
+
*/
|
|
1064
|
+
async function isServerAnswer(response) {
|
|
1065
|
+
try {
|
|
1066
|
+
const text = await response.clone().text();
|
|
1067
|
+
const parsed = JSON.parse(text);
|
|
1068
|
+
const error = parsed?.error;
|
|
1069
|
+
return parsed?.jsonrpc === '2.0' && typeof error?.code === 'number';
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
//# sourceMappingURL=pool.js.map
|