@sema-agent/core 7.5.1 → 7.6.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/CHANGELOG.md +61 -0
- package/dist/agents/cascade.d.ts +2 -2
- package/dist/agents/cascade.js +12 -10
- package/dist/agents/repair-loop.d.ts +5 -3
- package/dist/agents/repair-loop.js +13 -15
- package/dist/agents/subagent.d.ts +24 -42
- package/dist/agents/subagent.js +119 -105
- package/dist/agents/suspend-guard.d.ts +31 -19
- package/dist/agents/suspend-guard.js +14 -8
- package/dist/agents/teacher.js +9 -9
- package/dist/agents/team.d.ts +4 -3
- package/dist/agents/team.js +10 -8
- package/dist/agents/verify.d.ts +3 -3
- package/dist/agents/verify.js +17 -17
- package/dist/core/a2a.js +2 -1
- package/dist/core/ask-origin.d.ts +60 -7
- package/dist/core/ask-origin.js +26 -1
- package/dist/core/checkpoint-store.d.ts +78 -76
- package/dist/core/checkpoint-store.js +17 -1
- package/dist/core/gate-outcome.d.ts +189 -0
- package/dist/core/gate-outcome.js +70 -0
- package/dist/core/hooks.d.ts +18 -92
- package/dist/core/hooks.js +88 -85
- package/dist/core/mcp-failure.d.ts +104 -0
- package/dist/core/mcp-failure.js +128 -0
- package/dist/core/mcp.d.ts +21 -77
- package/dist/core/mcp.js +76 -150
- package/dist/core/pause-registry.d.ts +131 -0
- package/dist/core/pause-registry.js +27 -0
- package/dist/core/protocol-table.d.ts +5 -0
- package/dist/core/protocol-table.js +1 -0
- package/dist/core/runner/abort-race.d.ts +41 -0
- package/dist/core/runner/abort-race.js +38 -0
- package/dist/core/runner/assemble-result.d.ts +32 -41
- package/dist/core/runner/assemble-result.js +55 -74
- package/dist/core/runner/checkpoint-scope.d.ts +15 -3
- package/dist/core/runner/checkpoint-scope.js +3 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/content-ask-bindings.d.ts +27 -0
- package/dist/core/runner/content-ask-bindings.js +1 -0
- package/dist/core/runner/contracts.d.ts +144 -50
- package/dist/core/runner/denial-limit-arms.d.ts +24 -1
- package/dist/core/runner/denial-limit-arms.js +24 -3
- package/dist/core/runner/gate-exit.d.ts +74 -0
- package/dist/core/runner/gate-exit.js +55 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
- package/dist/core/runner/inherited-ask-grants.js +29 -0
- package/dist/core/runner/park-commit.d.ts +102 -0
- package/dist/core/runner/park-commit.js +31 -0
- package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
- package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
- package/dist/core/runner/prepare-ask-lane.js +131 -0
- package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
- package/dist/core/runner/prepare-boundary-parks.js +169 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-context-lane.d.ts +119 -0
- package/dist/core/runner/prepare-context-lane.js +230 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
- package/dist/core/runner/prepare-gate-stations.js +265 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
- package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
- package/dist/core/runner/prepare-memory-engine-session.js +233 -0
- package/dist/core/runner/prepare-memory.d.ts +44 -26
- package/dist/core/runner/prepare-park-ask.d.ts +162 -0
- package/dist/core/runner/prepare-park-ask.js +377 -0
- package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
- package/dist/core/runner/prepare-policy-chain.js +584 -0
- package/dist/core/runner/prepare-project-context.d.ts +1 -13
- package/dist/core/runner/prepare-project-context.js +1 -3
- package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
- package/dist/core/runner/prepare-prompt-assembly.js +162 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
- package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
- package/dist/core/runner/prepare-protocol-tools.js +0 -3
- package/dist/core/runner/prepare-question-face.d.ts +3 -21
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
- package/dist/core/runner/prepare-safety-scan.js +1 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
- package/dist/core/runner/prepare-suspend-saga.js +308 -0
- package/dist/core/runner/prepare-task.d.ts +9 -136
- package/dist/core/runner/prepare-task.js +47 -2745
- package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
- package/dist/core/runner/prepare-turn-wiring.js +201 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
- package/dist/core/runner/prepare-wiring-manifest.js +16 -10
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
- package/dist/core/runner/prepare-workspace-restore.js +3 -16
- package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
- package/dist/core/runner/prompt-hash-salt.js +2 -0
- package/dist/core/runner/remote-env-retry.d.ts +29 -0
- package/dist/core/runner/remote-env-retry.js +16 -0
- package/dist/core/runner/runtask.d.ts +16 -31
- package/dist/core/runner/runtask.js +110 -121
- package/dist/core/runner/terminal-projection.d.ts +22 -0
- package/dist/core/runner/terminal-projection.js +28 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +3 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
- package/dist/core/terminal-cause.d.ts +137 -0
- package/dist/core/terminal-cause.js +9 -0
- package/dist/core/tool-policy.d.ts +43 -139
- package/dist/core/tool-policy.js +79 -112
- package/dist/core/types.d.ts +67 -164
- package/dist/core/wiring-manifest.d.ts +6 -3
- package/dist/core/workflow-journal-store.js +3 -4
- package/dist/engine/harness/agent-harness.d.ts +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/orchestration/builtin-workflows.d.ts +2 -2
- package/dist/orchestration/builtin-workflows.js +1 -1
- package/dist/orchestration/goal.js +8 -7
- package/dist/orchestration/run-spec.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +4 -4
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-governance.js +4 -2
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +1 -1
- package/dist/orchestration/workflow.d.ts +11 -0
- package/dist/orchestration/workflow.js +64 -39
- package/dist/prompts/supervisor.d.ts +1 -1
- package/dist/prompts/supervisor.js +3 -3
- package/dist/scenarios/scenario-registry.js +1 -1
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +74 -22
- /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
- /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
package/dist/core/mcp.js
CHANGED
|
@@ -3,7 +3,8 @@ import { MCP_NAMESPACE } from "./protocol-table.js";
|
|
|
3
3
|
import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
|
|
4
4
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
5
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
-
import { StreamableHTTPClientTransport
|
|
6
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
+
import { classifyMcpFailure, describeHttpTransportFailure } from "./mcp-failure.js";
|
|
7
8
|
import { lstat, mkdir, writeFile } from "node:fs/promises";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
9
10
|
import { join } from "node:path";
|
|
@@ -204,6 +205,10 @@ function armMcpIdleWatchdog(health, idleMs, outerSignal) {
|
|
|
204
205
|
export function mcpStartupTimeoutMs() {
|
|
205
206
|
return parseEnvMs("MCP_TIMEOUT");
|
|
206
207
|
}
|
|
208
|
+
function markMcpFailure(e, details) {
|
|
209
|
+
e.details = details;
|
|
210
|
+
return e;
|
|
211
|
+
}
|
|
207
212
|
const MCP_SPEC_ERROR_CODE_NAMES = new Map([
|
|
208
213
|
[-32020, "header mismatch (the server saw an HTTP header that disagreed with the tool parameter mapped onto it)"],
|
|
209
214
|
[-32021, "missing required client capability (the server requires a capability this client does not declare)"],
|
|
@@ -237,90 +242,6 @@ function remoteErrorText(err, opts) {
|
|
|
237
242
|
const raw = err instanceof Error ? err.message : String(err);
|
|
238
243
|
return redactSecrets(opts?.collapse === true ? collapseMcpErrorPrefix(raw) : raw);
|
|
239
244
|
}
|
|
240
|
-
function isTransportLost(err) {
|
|
241
|
-
if (err instanceof McpError && err.code === ErrorCode.ConnectionClosed)
|
|
242
|
-
return true;
|
|
243
|
-
return err instanceof Error && /not connected|connection closed/i.test(err.message);
|
|
244
|
-
}
|
|
245
|
-
const NETWORK_CODES_NEVER_DELIVERED = new Set([
|
|
246
|
-
"ECONNREFUSED",
|
|
247
|
-
"ENOTFOUND",
|
|
248
|
-
"EAI_AGAIN",
|
|
249
|
-
"EHOSTUNREACH",
|
|
250
|
-
"ENETUNREACH",
|
|
251
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
252
|
-
]);
|
|
253
|
-
export function networkErrorCode(err, depth = 0) {
|
|
254
|
-
if (depth > 5 || !(err instanceof Error))
|
|
255
|
-
return undefined;
|
|
256
|
-
const code = err.code;
|
|
257
|
-
if (typeof code === "string" && /^(?:E[A-Z_]+|UND_ERR_[A-Z_]+)$/.test(code))
|
|
258
|
-
return code;
|
|
259
|
-
if (err instanceof AggregateError) {
|
|
260
|
-
for (const inner of err.errors) {
|
|
261
|
-
const found = networkErrorCode(inner, depth + 1);
|
|
262
|
-
if (found !== undefined)
|
|
263
|
-
return found;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return networkErrorCode(err.cause, depth + 1);
|
|
267
|
-
}
|
|
268
|
-
export function describeHttpTransportFailure(err) {
|
|
269
|
-
if (err instanceof McpError)
|
|
270
|
-
return undefined;
|
|
271
|
-
if (err instanceof StreamableHTTPError) {
|
|
272
|
-
const status = typeof err.code === "number" && err.code > 0 ? err.code : undefined;
|
|
273
|
-
return {
|
|
274
|
-
condition: status !== undefined
|
|
275
|
-
? `its HTTP endpoint answered ${status} instead of an MCP response`
|
|
276
|
-
: "its HTTP endpoint answered something that is not an MCP response",
|
|
277
|
-
delivered: "unknown",
|
|
278
|
-
...(status !== undefined ? { httpStatus: status } : {}),
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
const code = networkErrorCode(err);
|
|
282
|
-
if (code !== undefined) {
|
|
283
|
-
return NETWORK_CODES_NEVER_DELIVERED.has(code)
|
|
284
|
-
? { condition: `its HTTP endpoint could not be reached (${code})`, delivered: "no" }
|
|
285
|
-
: { condition: `the connection to its HTTP endpoint failed (${code})`, delivered: "unknown" };
|
|
286
|
-
}
|
|
287
|
-
if (err instanceof TypeError && /fetch failed|terminated|network/i.test(err.message)) {
|
|
288
|
-
return { condition: "the HTTP request to its endpoint failed at the network layer", delivered: "unknown" };
|
|
289
|
-
}
|
|
290
|
-
return undefined;
|
|
291
|
-
}
|
|
292
|
-
export const MCP_FAILURE_CODES = ["connect_refused", "connection_failed", "network", "not_mcp_response", "spawn_failed", "connection_closed", "timeout", "protocol", "invalid_config", "unknown"];
|
|
293
|
-
export function mcpFailureCodeOf(err, transport) {
|
|
294
|
-
if (err instanceof McpError) {
|
|
295
|
-
if (err.code === ErrorCode.ConnectionClosed)
|
|
296
|
-
return "connection_closed";
|
|
297
|
-
if (err.code === ErrorCode.RequestTimeout)
|
|
298
|
-
return "timeout";
|
|
299
|
-
return "protocol";
|
|
300
|
-
}
|
|
301
|
-
const http = describeHttpTransportFailure(err);
|
|
302
|
-
if (http !== undefined) {
|
|
303
|
-
if (http.httpStatus !== undefined)
|
|
304
|
-
return `http_${http.httpStatus}`;
|
|
305
|
-
if (err instanceof StreamableHTTPError)
|
|
306
|
-
return "not_mcp_response";
|
|
307
|
-
const code = networkErrorCode(err);
|
|
308
|
-
if (code === undefined)
|
|
309
|
-
return "network";
|
|
310
|
-
if (code === "ERR_INVALID_URL")
|
|
311
|
-
return "invalid_config";
|
|
312
|
-
if (code.startsWith("ERR_"))
|
|
313
|
-
return "unknown";
|
|
314
|
-
if (transport === "stdio")
|
|
315
|
-
return "spawn_failed";
|
|
316
|
-
if (transport === undefined)
|
|
317
|
-
return "connection_failed";
|
|
318
|
-
return http.delivered === "no" ? "connect_refused" : "connection_failed";
|
|
319
|
-
}
|
|
320
|
-
if (isTransportLost(err))
|
|
321
|
-
return "connection_closed";
|
|
322
|
-
return "unknown";
|
|
323
|
-
}
|
|
324
245
|
function writeEffectWarning(writeEffect) {
|
|
325
246
|
return writeEffect
|
|
326
247
|
? " This tool is write-capable: treat its side effects as POSSIBLY APPLIED and verify the actual state before retrying."
|
|
@@ -341,72 +262,63 @@ function rethrowHonestMcpError(err, ctx) {
|
|
|
341
262
|
sanitizeMcpErrorTextInPlace(err);
|
|
342
263
|
if (ctx.idle?.signal.reason === IDLE_WATCHDOG_ABORT_REASON) {
|
|
343
264
|
const serverLabel = inlineUntrusted(ctx.server);
|
|
344
|
-
|
|
265
|
+
finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" received no response for ${ctx.idle.idleMs}ms (idle watchdog — ` +
|
|
345
266
|
`distinct from the ${ctx.timeoutMs}ms total ceiling above). The server may still be alive but is not ` +
|
|
346
267
|
`responding; the client stopped waiting.${writeEffectWarning(ctx.writeEffect)} (Set MCP_IDLE_TIMEOUT_STDIO ` +
|
|
347
|
-
`/ MCP_IDLE_TIMEOUT_HTTP (ms) to change this bound.)`, { cause: err });
|
|
348
|
-
e.errorKind = "timeout";
|
|
349
|
-
e.details = { timedOut: true, timeoutMs: ctx.idle.idleMs, idleTimeout: true, server: ctx.server };
|
|
350
|
-
finish(e);
|
|
268
|
+
`/ MCP_IDLE_TIMEOUT_HTTP (ms) to change this bound.)`, { cause: err }), { code: "timeout", delivered: "unknown", server: ctx.server, timeoutMs: ctx.idle.idleMs, idleTimeout: true }));
|
|
351
269
|
}
|
|
352
270
|
if (ctx.signal?.aborted)
|
|
353
271
|
finish(err);
|
|
354
272
|
const serverLabel = inlineUntrusted(ctx.server);
|
|
355
273
|
const fenceServerText = (label, raw) => delimitUntrusted(label, truncateMcpErrorText(raw));
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
274
|
+
const failure = classifyMcpFailure(err, { phase: "request" });
|
|
275
|
+
const details = (extra) => ({ code: failure.kind, delivered: failure.delivered, server: ctx.server, ...extra });
|
|
276
|
+
switch (failure.kind) {
|
|
277
|
+
case "timeout": {
|
|
278
|
+
const data = err.data;
|
|
279
|
+
const totalMs = typeof data?.maxTotalTimeout === "number" ? data.maxTotalTimeout : undefined;
|
|
280
|
+
finish(markMcpFailure(new Error(totalMs !== undefined
|
|
281
|
+
? `${ctx.what} on MCP server "${serverLabel}" exceeded its total time ceiling of ${totalMs}ms (the call stayed alive — e.g. via progress notifications — but ran past the total wall-clock budget). The client stopped waiting, but the server may still be executing the request — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)} (Set the MCP_TOOL_TIMEOUT_TOTAL environment variable (ms) to change this ceiling; MCP_TOOL_TIMEOUT only tunes the per-call timer, currently ${ctx.timeoutMs}ms.)`
|
|
282
|
+
: `${ctx.what} on MCP server "${serverLabel}" timed out after ${ctx.timeoutMs}ms. The client stopped waiting, but the server may still be executing the request — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)} (Set the MCP_TOOL_TIMEOUT environment variable (ms) to change this limit.)`, { cause: err }), totalMs !== undefined ? details({ timeoutMs: totalMs, totalTimeout: true }) : details({ timeoutMs: ctx.timeoutMs })));
|
|
283
|
+
}
|
|
284
|
+
case "connection_closed":
|
|
285
|
+
finish(markMcpFailure(new Error(`The connection to MCP server "${serverLabel}" was lost while ${ctx.what} was in flight. The request may or may not have executed on the server — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)}`, { cause: err }), details()));
|
|
286
|
+
case "http_status":
|
|
287
|
+
case "not_mcp_response":
|
|
288
|
+
case "connect_refused":
|
|
289
|
+
case "connection_failed": {
|
|
290
|
+
const httpFailure = describeHttpTransportFailure(err);
|
|
291
|
+
const detail = remoteErrorText(err);
|
|
292
|
+
const fenced = `\nThe transport error follows as external/untrusted data:\n${fenceServerText(`${ctx.server} transport error`, detail)}`;
|
|
293
|
+
finish(markMcpFailure(new Error(failure.delivered === "no"
|
|
294
|
+
? `${ctx.what} could not reach MCP server "${serverLabel}": ${httpFailure.condition}. The request was not delivered, so the server did not execute it. This server's tools and resources will keep failing until its endpoint is reachable again — do not retry them; use an alternative if one exists.${fenced}`
|
|
295
|
+
: `${ctx.what} failed at the transport layer of MCP server "${serverLabel}": ${httpFailure.condition}. The request may or may not have executed on the server — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)}${fenced}`, { cause: err }), details(failure.httpStatus !== undefined ? { httpStatus: failure.httpStatus } : undefined)));
|
|
296
|
+
}
|
|
297
|
+
case "protocol": {
|
|
298
|
+
const code = err.code;
|
|
299
|
+
const condition = describeMcpSpecErrorCode(code);
|
|
300
|
+
if (condition !== undefined) {
|
|
301
|
+
finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" was rejected with MCP protocol error ${code} — ${condition}. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, err.message)}`, { cause: err }), details({ specErrorCode: code })));
|
|
302
|
+
}
|
|
303
|
+
if (ctx.attributeServer) {
|
|
304
|
+
finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" failed. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, remoteErrorText(err))}`, { cause: err }), details()));
|
|
305
|
+
}
|
|
306
|
+
finish(markMcpFailure(err, details()));
|
|
307
|
+
}
|
|
308
|
+
case "spawn_failed":
|
|
309
|
+
case "invalid_config":
|
|
310
|
+
case "unknown": {
|
|
311
|
+
if (ctx.attributeServer) {
|
|
312
|
+
finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" failed. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, remoteErrorText(err))}`, { cause: err }), details()));
|
|
313
|
+
}
|
|
314
|
+
if (err instanceof Error)
|
|
315
|
+
finish(markMcpFailure(err, details()));
|
|
316
|
+
finish(err);
|
|
397
317
|
}
|
|
398
318
|
}
|
|
399
|
-
if (ctx.attributeServer) {
|
|
400
|
-
const msg = remoteErrorText(err);
|
|
401
|
-
finish(new Error(`${ctx.what} on MCP server "${serverLabel}" failed. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, msg)}`, { cause: err }));
|
|
402
|
-
}
|
|
403
|
-
finish(err);
|
|
404
319
|
}
|
|
405
320
|
function throwDeadServer(server, what) {
|
|
406
|
-
|
|
407
|
-
e.errorKind = "server_disconnected";
|
|
408
|
-
e.details = { server };
|
|
409
|
-
throw e;
|
|
321
|
+
throw markMcpFailure(new Error(`MCP server "${inlineUntrusted(server)}" is disconnected (its transport closed earlier in this task). ${what} was not attempted. This server's tools and resources will keep failing until the server is available again — do not retry them; use an alternative if one exists.`), { code: "connection_closed", delivered: "no", server });
|
|
410
322
|
}
|
|
411
323
|
export { normalizeNameSegment as normalizeMcpName, clampNameSegment } from "./protocol-naming.js";
|
|
412
324
|
export * from "./image-downsample.js";
|
|
@@ -516,6 +428,7 @@ export function resolveProtocolHttpHeaders(protocol, t, principal) {
|
|
|
516
428
|
}
|
|
517
429
|
return t.headers;
|
|
518
430
|
}
|
|
431
|
+
export const declaredEndpointFetch = (url, init) => fetch(url, { ...init, redirect: "manual" });
|
|
519
432
|
function buildTransport(spec, principal, kind) {
|
|
520
433
|
const t = spec.transport;
|
|
521
434
|
if (kind === "stdio") {
|
|
@@ -532,6 +445,7 @@ function buildTransport(spec, principal, kind) {
|
|
|
532
445
|
const headers = resolveProtocolHttpHeaders(MCP_NAMESPACE.id, t, principal);
|
|
533
446
|
return new StreamableHTTPClientTransport(new URL(t.url), {
|
|
534
447
|
requestInit: headers ? { headers } : undefined,
|
|
448
|
+
fetch: declaredEndpointFetch,
|
|
535
449
|
});
|
|
536
450
|
}
|
|
537
451
|
let autoResizer;
|
|
@@ -893,7 +807,8 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
893
807
|
}
|
|
894
808
|
else {
|
|
895
809
|
warnings.push(asServerWarning(spec, r.reason));
|
|
896
|
-
|
|
810
|
+
const { kind, delivered, httpStatus } = classifyMcpFailure(r.reason, { phase: "connect", transport: dialKinds[i] });
|
|
811
|
+
statuses.push({ name: spec.name, status: "failed", errorCode: kind, delivered, ...(httpStatus !== undefined ? { httpStatus } : {}), error: namedMcpFailureText(r.reason) });
|
|
897
812
|
}
|
|
898
813
|
}
|
|
899
814
|
const resourceTools = buildResourceTools(resourceServers, isServerRevoked);
|
|
@@ -933,8 +848,8 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
933
848
|
const advertised = new Set(listed.tools.map((t) => t.name));
|
|
934
849
|
const retainedRaw = listed.incomplete !== undefined ? h.listedRaw.filter((t) => !advertised.has(t.name)) : [];
|
|
935
850
|
const mergedRaw = retainedRaw.length > 0 ? [...listed.tools, ...retainedRaw] : listed.tools;
|
|
936
|
-
|
|
937
|
-
|
|
851
|
+
const { serverTools, serverAxes, dropped, accepted } = intakeListedTools({ tools: mergedRaw }, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked, h.transportKind);
|
|
852
|
+
cacheMcpToolMetadata(h.client, accepted);
|
|
938
853
|
const priorNames = h.tools.map((t) => t.name);
|
|
939
854
|
const newNames = serverTools.map((t) => t.name);
|
|
940
855
|
const added = newNames.filter((n) => !priorNames.includes(n));
|
|
@@ -1305,7 +1220,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
|
1305
1220
|
if (isServerRevoked(server)) {
|
|
1306
1221
|
return {
|
|
1307
1222
|
content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. This server stays on this run's tool roster — every call to it is refused the same way, so don't retry it.` }],
|
|
1308
|
-
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
|
|
1223
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", delivered: "no", server },
|
|
1309
1224
|
terminate: false,
|
|
1310
1225
|
isError: true,
|
|
1311
1226
|
};
|
|
@@ -1364,7 +1279,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
|
1364
1279
|
if (isServerRevoked(server)) {
|
|
1365
1280
|
return {
|
|
1366
1281
|
content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. This server stays on this run's tool roster — every call to it is refused the same way, so don't retry it.` }],
|
|
1367
|
-
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
|
|
1282
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", delivered: "no", server },
|
|
1368
1283
|
terminate: false,
|
|
1369
1284
|
isError: true,
|
|
1370
1285
|
};
|
|
@@ -1524,8 +1439,8 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1524
1439
|
announce.fn?.();
|
|
1525
1440
|
};
|
|
1526
1441
|
const listed = await listToolsLenient(client, startupOpts);
|
|
1527
|
-
|
|
1528
|
-
|
|
1442
|
+
const { serverTools, serverAxes, dropped, advisories, accepted } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked, transportKind);
|
|
1443
|
+
cacheMcpToolMetadata(client, accepted);
|
|
1529
1444
|
const caps = client.getServerCapabilities();
|
|
1530
1445
|
const resourceInfo = caps?.resources
|
|
1531
1446
|
? {
|
|
@@ -1567,6 +1482,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1567
1482
|
const serverTools = [];
|
|
1568
1483
|
const serverAxes = [];
|
|
1569
1484
|
const dropped = [];
|
|
1485
|
+
const accepted = [];
|
|
1570
1486
|
const advisories = [];
|
|
1571
1487
|
const mintedNames = new Map();
|
|
1572
1488
|
for (const t of listed.tools) {
|
|
@@ -1607,6 +1523,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1607
1523
|
advisories.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaAdvisory, 240) });
|
|
1608
1524
|
}
|
|
1609
1525
|
const remoteName = t.name;
|
|
1526
|
+
const taskRequired = t.execution !== null && typeof t.execution === "object" && t.execution.taskSupport === "required";
|
|
1610
1527
|
const namespacedName = mintNamespacedToolName(MCP_NAMESPACE, spec.name, remoteName);
|
|
1611
1528
|
const mintedBy = mintedNames.get(namespacedName);
|
|
1612
1529
|
if (mintedBy !== undefined) {
|
|
@@ -1639,7 +1556,15 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1639
1556
|
if (isServerRevoked?.(spec.name) === true) {
|
|
1640
1557
|
return {
|
|
1641
1558
|
content: [{ type: "text", text: `The call to MCP server "${spec.name}" was refused: the server was revoked by the operator mid-session. The call was NOT sent, so the server did not execute it. This server stays on this run's tool roster — every call to it is refused the same way, so don't retry it.` }],
|
|
1642
|
-
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server: spec.name },
|
|
1559
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", delivered: "no", server: spec.name },
|
|
1560
|
+
terminate: false,
|
|
1561
|
+
isError: true,
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
if (taskRequired) {
|
|
1565
|
+
return {
|
|
1566
|
+
content: [{ type: "text", text: "The call was refused: this tool requires task-based execution (MCP tasks), which this client does not drive. The call was NOT sent, so the server did not execute it. Every call to this tool is refused the same way, so don't retry it." }],
|
|
1567
|
+
details: { error: "mcp.task_required", code: "mcp.task_required", delivered: "no", server: spec.name },
|
|
1643
1568
|
terminate: false,
|
|
1644
1569
|
isError: true,
|
|
1645
1570
|
};
|
|
@@ -1693,7 +1618,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1693
1618
|
if (reminderDisclosure !== undefined) {
|
|
1694
1619
|
observeReminderMarkEcho({ text: msg, mark: reminderDisclosure.mark, outlet: "mcp", counts: reminderDisclosure.counts });
|
|
1695
1620
|
}
|
|
1696
|
-
throw new Error(msg);
|
|
1621
|
+
throw Object.assign(new Error(msg), { details: { code: "mcp.tool_error", delivered: "yes", server: spec.name } });
|
|
1697
1622
|
}
|
|
1698
1623
|
const content = gateMcpOutput(mapped);
|
|
1699
1624
|
const sc = res.structuredContent;
|
|
@@ -1731,8 +1656,9 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1731
1656
|
},
|
|
1732
1657
|
};
|
|
1733
1658
|
serverTools.push(declaredContentOrigin !== undefined ? withContentOrigin(mounted, declaredContentOrigin) : mounted);
|
|
1659
|
+
accepted.push(t);
|
|
1734
1660
|
}
|
|
1735
|
-
return { serverTools, serverAxes, dropped, advisories };
|
|
1661
|
+
return { serverTools, serverAxes, dropped, advisories, accepted };
|
|
1736
1662
|
}
|
|
1737
1663
|
function asServerWarning(spec, err) {
|
|
1738
1664
|
const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: sanitizedCause(err) });
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pause registry — ONE table for the three faces of a durable pause (why it stopped / what it left
|
|
3
|
+
* hanging / what brings it back), the three word types and the resume match DERIVED from it, and the
|
|
4
|
+
* registry-bound `gate` × `pendingAction` pair with its checked constructor. A vocabulary module on the
|
|
5
|
+
* floor: it types against `checkpoint-store.ts` (type-only, no runtime edge) and `checkpoint-store.ts`
|
|
6
|
+
* reaches down for the pair type.
|
|
7
|
+
*/
|
|
8
|
+
import type { AssertAllKeysHandled } from "./ask-origin.js";
|
|
9
|
+
import type { Checkpoint, CheckpointGate, CheckpointRow, PendingAction, ResumeOutcome } from "./checkpoint-store.js";
|
|
10
|
+
/**
|
|
11
|
+
* THE registry of pauses — one row per way a run can stop and wait, and the ONE place the three faces
|
|
12
|
+
* of that pause are declared together: WHY it stopped (`CheckpointGate.kind`, the row key), WHAT it left
|
|
13
|
+
* hanging (`pending` — the {@link PendingAction} kind the row carries), and WHAT BRINGS IT BACK (`resume`
|
|
14
|
+
* — the {@link ResumeOutcome} gate word a caller must answer with). The three vocabularies used to be
|
|
15
|
+
* three independent `as const` lists whose correspondence lived in a six-arm disjunction in the resume
|
|
16
|
+
* entry and two FACET comments; adding a kind of pause meant three declarations, one disjunction arm and
|
|
17
|
+
* two comments, and missing any of them was silent. Now the three word types, the resume match and the
|
|
18
|
+
* facet are DERIVED from this table: adding a pause = adding a row, and the fences below red the build
|
|
19
|
+
* until every declaration agrees with it.
|
|
20
|
+
*
|
|
21
|
+
* - `facet` — the deadline semantic a deployment's reaper applies (see {@link Checkpoint.deadline}):
|
|
22
|
+
* `A` = approval gates, whose `deadline` is an SLA resolve-deny; `B` = unattended-TTL gates, whose
|
|
23
|
+
* `deadline` is an abandonment TTL (`expire`/`reap` only — never a resolve-deny; these pauses produce
|
|
24
|
+
* no `tool_end` and therefore no settlement); `-` = the non-gate park (`task_done`).
|
|
25
|
+
* - `taskStatus` — the `TaskResult.status` word a run assembles when it stops at this gate (approvals and
|
|
26
|
+
* resource slices read `suspended`; the two review pauses read `needs_review`).
|
|
27
|
+
* - `terminal` — `task_done` is the 1C handle / deployment-park convention, not a run pause core mints.
|
|
28
|
+
*/
|
|
29
|
+
export declare const PAUSE_REGISTRY: {
|
|
30
|
+
readonly human: {
|
|
31
|
+
readonly pending: "tool_approval";
|
|
32
|
+
readonly resume: "policy_ask";
|
|
33
|
+
readonly facet: "A";
|
|
34
|
+
readonly taskStatus: "suspended";
|
|
35
|
+
readonly terminal: false;
|
|
36
|
+
};
|
|
37
|
+
readonly irreversible_ask: {
|
|
38
|
+
readonly pending: "tool_approval";
|
|
39
|
+
readonly resume: "policy_ask";
|
|
40
|
+
readonly facet: "A";
|
|
41
|
+
readonly taskStatus: "suspended";
|
|
42
|
+
readonly terminal: false;
|
|
43
|
+
};
|
|
44
|
+
readonly resource_limit: {
|
|
45
|
+
readonly pending: "resource_limit";
|
|
46
|
+
readonly resume: "resource_limit";
|
|
47
|
+
readonly facet: "B";
|
|
48
|
+
readonly taskStatus: "suspended";
|
|
49
|
+
readonly terminal: false;
|
|
50
|
+
};
|
|
51
|
+
readonly needs_review: {
|
|
52
|
+
readonly pending: "review";
|
|
53
|
+
readonly resume: "dry_run_review";
|
|
54
|
+
readonly facet: "B";
|
|
55
|
+
readonly taskStatus: "needs_review";
|
|
56
|
+
readonly terminal: false;
|
|
57
|
+
};
|
|
58
|
+
readonly plan_review: {
|
|
59
|
+
readonly pending: "plan_review";
|
|
60
|
+
readonly resume: "plan_review";
|
|
61
|
+
readonly facet: "B";
|
|
62
|
+
readonly taskStatus: "needs_review";
|
|
63
|
+
readonly terminal: false;
|
|
64
|
+
};
|
|
65
|
+
readonly task_done: {
|
|
66
|
+
readonly pending: "task_done";
|
|
67
|
+
readonly resume: "task_done";
|
|
68
|
+
readonly facet: "-";
|
|
69
|
+
readonly taskStatus: "suspended";
|
|
70
|
+
readonly terminal: true;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/** The pause kinds — the row keys of {@link PAUSE_REGISTRY}; `CheckpointGate.kind` is fenced to equal it. */
|
|
74
|
+
export type GateKind = keyof typeof PAUSE_REGISTRY;
|
|
75
|
+
/** The pending-action kinds — the `pending` column; `PendingAction.kind` is fenced to equal it. */
|
|
76
|
+
export type PendingKind = (typeof PAUSE_REGISTRY)[GateKind]["pending"];
|
|
77
|
+
/** The resume gate words — the `resume` column plus `wake` (the un-park verb, which is not a gate decision
|
|
78
|
+
* and so has no registry row); `ResumeOutcome.gate` is fenced to equal it. */
|
|
79
|
+
export type ResumeGate = (typeof PAUSE_REGISTRY)[GateKind]["resume"] | "wake";
|
|
80
|
+
/** Runtime twin of {@link GateKind}. */
|
|
81
|
+
export declare function isGateKind(v: unknown): v is GateKind;
|
|
82
|
+
/** The one resume-match predicate: does this outcome gate word answer a checkpoint of this kind? `wake`
|
|
83
|
+
* answers any (it passed its own, stricter, non-gate validation upstream); every other word must be the
|
|
84
|
+
* row's `resume` column. Replaces a six-arm hand-written disjunction. */
|
|
85
|
+
export declare function resumeGateMatches(gateKind: GateKind, outcomeGate: ResumeGate): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The one CONSTRUCTOR of a registry-bound pause pair from a gate and a pending action that arrive
|
|
88
|
+
* separately typed (a mint site that chooses its gate in a ternary, a store deserializing a row, a test
|
|
89
|
+
* fixture taking overrides). The generic binds the pending action's kind to the gate's registry row at
|
|
90
|
+
* the call site; the runtime check is the belt for the wide call (`CheckpointGate` × `PendingAction`) and
|
|
91
|
+
* for a deserialized row, where the type says nothing about the bytes. A pair the registry does not pair
|
|
92
|
+
* is refused loudly here rather than persisted and discovered at resume.
|
|
93
|
+
*/
|
|
94
|
+
export declare function pauseOf<K extends GateKind>(gate: Extract<CheckpointGate, {
|
|
95
|
+
kind: K;
|
|
96
|
+
}>, pendingAction: Extract<PendingAction, {
|
|
97
|
+
kind: (typeof PAUSE_REGISTRY)[K]["pending"];
|
|
98
|
+
}>): CheckpointPause<K>;
|
|
99
|
+
/** The loose input shape of {@link checkpointFrom}: a row's fields with its gate and pending action typed
|
|
100
|
+
* independently (as a deserializer or a fixture holds them). */
|
|
101
|
+
export type CheckpointFields = CheckpointRow & {
|
|
102
|
+
gate: CheckpointGate;
|
|
103
|
+
pendingAction: PendingAction;
|
|
104
|
+
};
|
|
105
|
+
/** Build a {@link Checkpoint} from independently typed fields — {@link pauseOf} binds (and checks) the pair. */
|
|
106
|
+
export declare function checkpointFrom(fields: CheckpointFields): Checkpoint;
|
|
107
|
+
/** Compile-time fences (each `never` while the declaration and the registry agree, in BOTH directions). */
|
|
108
|
+
export type GateDeclarationCoversEveryRegistryRow = AssertAllKeysHandled<Exclude<GateKind, CheckpointGate["kind"]>>;
|
|
109
|
+
export type RegistryCoversEveryGateDeclaration = AssertAllKeysHandled<Exclude<CheckpointGate["kind"], GateKind>>;
|
|
110
|
+
export type PendingDeclarationCoversEveryRegistryColumn = AssertAllKeysHandled<Exclude<PendingKind, PendingAction["kind"]>>;
|
|
111
|
+
export type RegistryCoversEveryPendingDeclaration = AssertAllKeysHandled<Exclude<PendingAction["kind"], PendingKind>>;
|
|
112
|
+
export type ResumeDeclarationCoversEveryRegistryColumn = AssertAllKeysHandled<Exclude<ResumeGate, ResumeOutcome["gate"]>>;
|
|
113
|
+
export type RegistryCoversEveryResumeDeclaration = AssertAllKeysHandled<Exclude<ResumeOutcome["gate"], ResumeGate>>;
|
|
114
|
+
/**
|
|
115
|
+
* One row's `gate` × `pendingAction` pair, BOUND by {@link PAUSE_REGISTRY}: a `needs_review` gate carries a
|
|
116
|
+
* `review` pending action and nothing else, an approval gate a `tool_approval`, and so on. Distributed over
|
|
117
|
+
* the registry rows so that a row literal pairing a gate with another kind's pending action does not
|
|
118
|
+
* type-check — the invariant that used to be a sentence in a comment is now the shape of the type.
|
|
119
|
+
*/
|
|
120
|
+
type PairOf<K extends GateKind> = {
|
|
121
|
+
gate: Extract<CheckpointGate, {
|
|
122
|
+
kind: K;
|
|
123
|
+
}>;
|
|
124
|
+
pendingAction: Extract<PendingAction, {
|
|
125
|
+
kind: (typeof PAUSE_REGISTRY)[K]["pending"];
|
|
126
|
+
}>;
|
|
127
|
+
};
|
|
128
|
+
export type CheckpointPause<K extends GateKind = GateKind> = {
|
|
129
|
+
[P in K]: PairOf<P>;
|
|
130
|
+
}[K];
|
|
131
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const PAUSE_REGISTRY = {
|
|
2
|
+
human: { pending: "tool_approval", resume: "policy_ask", facet: "A", taskStatus: "suspended", terminal: false },
|
|
3
|
+
irreversible_ask: { pending: "tool_approval", resume: "policy_ask", facet: "A", taskStatus: "suspended", terminal: false },
|
|
4
|
+
resource_limit: { pending: "resource_limit", resume: "resource_limit", facet: "B", taskStatus: "suspended", terminal: false },
|
|
5
|
+
needs_review: { pending: "review", resume: "dry_run_review", facet: "B", taskStatus: "needs_review", terminal: false },
|
|
6
|
+
plan_review: { pending: "plan_review", resume: "plan_review", facet: "B", taskStatus: "needs_review", terminal: false },
|
|
7
|
+
task_done: { pending: "task_done", resume: "task_done", facet: "-", taskStatus: "suspended", terminal: true },
|
|
8
|
+
};
|
|
9
|
+
export function isGateKind(v) {
|
|
10
|
+
return typeof v === "string" && Object.prototype.hasOwnProperty.call(PAUSE_REGISTRY, v);
|
|
11
|
+
}
|
|
12
|
+
export function resumeGateMatches(gateKind, outcomeGate) {
|
|
13
|
+
return outcomeGate === "wake" || PAUSE_REGISTRY[gateKind].resume === outcomeGate;
|
|
14
|
+
}
|
|
15
|
+
export function pauseOf(gate, pendingAction) {
|
|
16
|
+
if (!isGateKind(gate.kind))
|
|
17
|
+
throw new Error(`checkpoint gate kind "${String(gate.kind)}" is not a registered pause`);
|
|
18
|
+
const expected = PAUSE_REGISTRY[gate.kind].pending;
|
|
19
|
+
if (pendingAction.kind !== expected) {
|
|
20
|
+
throw new Error(`a "${gate.kind}" checkpoint carries a "${String(pendingAction.kind)}" pending action — the pause registry pairs it with "${expected}"`);
|
|
21
|
+
}
|
|
22
|
+
return { gate, pendingAction };
|
|
23
|
+
}
|
|
24
|
+
export function checkpointFrom(fields) {
|
|
25
|
+
const { gate, pendingAction, ...row } = fields;
|
|
26
|
+
return { ...row, ...pauseOf(gate, pendingAction) };
|
|
27
|
+
}
|
|
@@ -75,6 +75,11 @@ export declare const A2A_NAMESPACE: ProtocolNamespace<typeof A2A_PREFIX_NAME>;
|
|
|
75
75
|
/** Every protocol namespace the engine knows. APPEND-ONLY: adding a protocol must not change how any
|
|
76
76
|
* existing name resolves — prefixes must stay pairwise non-overlapping (pinned in the table test). */
|
|
77
77
|
export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
|
|
78
|
+
/** The namespaced-name shapes the protocol table currently owns, rendered for the two messages that have
|
|
79
|
+
* to name them (the caller-name reservation and the policy audit's unprefixed-name arm). Read from the
|
|
80
|
+
* table rather than written out: a message that hard-codes ONE protocol becomes wrong — while staying
|
|
81
|
+
* green — the moment a second one is appended, and both messages tell a deployment what to write. */
|
|
82
|
+
export declare const NAMESPACED_NAME_SHAPES: string;
|
|
78
83
|
/** The namespace owning `name`, or `undefined` for a caller/first-party tool. */
|
|
79
84
|
export declare function protocolOf(name: string): ProtocolNamespace | undefined;
|
|
80
85
|
export {};
|
|
@@ -37,6 +37,7 @@ const A2A_PREFIX_NAME = `a2a${NAME_SEP}`;
|
|
|
37
37
|
export const MCP_NAMESPACE = makeProtocolNamespace("mcp", MCP_PREFIX_NAME);
|
|
38
38
|
export const A2A_NAMESPACE = makeProtocolNamespace("a2a", A2A_PREFIX_NAME);
|
|
39
39
|
export const PROTOCOL_TABLE = [MCP_NAMESPACE, A2A_NAMESPACE];
|
|
40
|
+
export const NAMESPACED_NAME_SHAPES = PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(", ");
|
|
40
41
|
export function protocolOf(name) {
|
|
41
42
|
return PROTOCOL_TABLE.find((ns) => name.startsWith(ns.prefix));
|
|
42
43
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The abort-signal RACING vocabulary the park lane's synchronous-wait seats stand on — the per-call signal
|
|
3
|
+
* composition, the value-mapping race and the tagged three-state race — moved whole out of the orchestrator so
|
|
4
|
+
* the ask-lane phase (adjudicate / resolveAsk), the suspend-saga phase (the paused-VM compensation) and the
|
|
5
|
+
* park-ask phase (the content-ask delivery race, the projection re-adjudication, the fence) reach DOWN for the
|
|
6
|
+
* same three helpers instead of each carrying a copy. Pure: no engine state, no clock.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* design/384 slice 1 — the per-call signal composition all three synchronous-wait seats share
|
|
10
|
+
* (adjudicate / resolveAsk / resolveContentAsk): `AbortSignal.any([run, call])` when the gate
|
|
11
|
+
* threaded a per-call signal, the bare run signal otherwise (host-driven gates and unthreaded emits
|
|
12
|
+
* keep the historical binding byte for byte). The run signal is ALSO composed into the loop's turn
|
|
13
|
+
* controller, so the double source is a belt, not a dependency — this layer must not assume the
|
|
14
|
+
* loop's internal wiring. The composed product lives exactly as long as the one wait it bounds
|
|
15
|
+
* (both sources outlive it), so nothing here retains across calls.
|
|
16
|
+
*/
|
|
17
|
+
export declare function composeCallSignal(runSignal: AbortSignal, callSignal: AbortSignal | undefined): AbortSignal;
|
|
18
|
+
/** design/384 slice 2 — how a raced await ended. The generalized sibling of the ask-side
|
|
19
|
+
* `raceAskWaitAgainstSignal` (tool-policy.ts): same three-state form over any value domain.
|
|
20
|
+
* `aborted` means the SIGNAL settled the race — the promise may still be pending and stays the
|
|
21
|
+
* caller's to detach. `threw` reports the rejection AS a rejection, never folded into the abort
|
|
22
|
+
* arm: the value-mapping `raceAbort` below resolves `onAbort()` for a rejection too, which at a
|
|
23
|
+
* DECISION seat would disguise a real failure as a concession (and feed the concession value into
|
|
24
|
+
* whatever door consumes the raced result) — a decision seat must race with THIS form. */
|
|
25
|
+
export type RacedSettlement<T> = {
|
|
26
|
+
tag: "value";
|
|
27
|
+
value: T;
|
|
28
|
+
} | {
|
|
29
|
+
tag: "threw";
|
|
30
|
+
error: unknown;
|
|
31
|
+
} | {
|
|
32
|
+
tag: "aborted";
|
|
33
|
+
};
|
|
34
|
+
/** design/384 slice 2 — settle a raced await the moment `signal` aborts, WITHOUT waiting for the
|
|
35
|
+
* promise to notice. Pre-aborted entry answers at once; the listener is removed on settle
|
|
36
|
+
* whichever side wins (the `raceAbort` hygiene: `{once}` alone leaks a closure on the signal when
|
|
37
|
+
* the promise settles first). The mapped promise attaches BOTH handlers up front, so an abort-arm
|
|
38
|
+
* return never leaves an unhandled rejection behind. */
|
|
39
|
+
export declare function raceSettlementAgainstSignal<T>(p: Promise<T>, signal: AbortSignal): Promise<RacedSettlement<T>>;
|
|
40
|
+
/** Race a promise against an abort signal; on abort, resolve with `onAbort()`. Cleans up its listener. */
|
|
41
|
+
export declare function raceAbort<T>(p: Promise<T>, signal: AbortSignal, onAbort: () => T): Promise<T>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function composeCallSignal(runSignal, callSignal) {
|
|
2
|
+
return callSignal !== undefined ? AbortSignal.any([runSignal, callSignal]) : runSignal;
|
|
3
|
+
}
|
|
4
|
+
export function raceSettlementAgainstSignal(p, signal) {
|
|
5
|
+
const settled = p.then((value) => ({ tag: "value", value }), (error) => ({ tag: "threw", error }));
|
|
6
|
+
if (signal.aborted)
|
|
7
|
+
return Promise.resolve({ tag: "aborted" });
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let done = false;
|
|
10
|
+
const finish = (r) => {
|
|
11
|
+
if (done)
|
|
12
|
+
return;
|
|
13
|
+
done = true;
|
|
14
|
+
signal.removeEventListener("abort", onAbortEvent);
|
|
15
|
+
resolve(r);
|
|
16
|
+
};
|
|
17
|
+
const onAbortEvent = () => finish({ tag: "aborted" });
|
|
18
|
+
signal.addEventListener("abort", onAbortEvent, { once: true });
|
|
19
|
+
void settled.then(finish);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function raceAbort(p, signal, onAbort) {
|
|
23
|
+
if (signal.aborted)
|
|
24
|
+
return Promise.resolve(onAbort());
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let settled = false;
|
|
27
|
+
const finish = (v) => {
|
|
28
|
+
if (settled)
|
|
29
|
+
return;
|
|
30
|
+
settled = true;
|
|
31
|
+
signal.removeEventListener("abort", onAbortEvent);
|
|
32
|
+
resolve(v);
|
|
33
|
+
};
|
|
34
|
+
const onAbortEvent = () => finish(onAbort());
|
|
35
|
+
signal.addEventListener("abort", onAbortEvent, { once: true });
|
|
36
|
+
p.then(finish, () => finish(onAbort()));
|
|
37
|
+
});
|
|
38
|
+
}
|