@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.
Files changed (132) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/protocol-table.d.ts +5 -0
  31. package/dist/core/protocol-table.js +1 -0
  32. package/dist/core/runner/abort-race.d.ts +41 -0
  33. package/dist/core/runner/abort-race.js +38 -0
  34. package/dist/core/runner/assemble-result.d.ts +32 -41
  35. package/dist/core/runner/assemble-result.js +55 -74
  36. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  37. package/dist/core/runner/checkpoint-scope.js +3 -0
  38. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  39. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  40. package/dist/core/runner/content-ask-bindings.js +1 -0
  41. package/dist/core/runner/contracts.d.ts +144 -50
  42. package/dist/core/runner/denial-limit-arms.d.ts +24 -1
  43. package/dist/core/runner/denial-limit-arms.js +24 -3
  44. package/dist/core/runner/gate-exit.d.ts +74 -0
  45. package/dist/core/runner/gate-exit.js +55 -0
  46. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  47. package/dist/core/runner/inherited-ask-grants.js +29 -0
  48. package/dist/core/runner/park-commit.d.ts +102 -0
  49. package/dist/core/runner/park-commit.js +31 -0
  50. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  51. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  52. package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
  53. package/dist/core/runner/prepare-ask-lane.js +131 -0
  54. package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
  55. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  56. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  57. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  58. package/dist/core/runner/prepare-context-lane.js +230 -0
  59. package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
  60. package/dist/core/runner/prepare-gate-stations.js +265 -0
  61. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  63. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  64. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  65. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  66. package/dist/core/runner/prepare-memory.d.ts +44 -26
  67. package/dist/core/runner/prepare-park-ask.d.ts +162 -0
  68. package/dist/core/runner/prepare-park-ask.js +377 -0
  69. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  70. package/dist/core/runner/prepare-policy-chain.js +584 -0
  71. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  72. package/dist/core/runner/prepare-project-context.js +1 -3
  73. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  74. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  75. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  76. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  77. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  78. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  79. package/dist/core/runner/prepare-question-face.js +2 -1
  80. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  81. package/dist/core/runner/prepare-safety-scan.js +1 -2
  82. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  83. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  84. package/dist/core/runner/prepare-task.d.ts +9 -136
  85. package/dist/core/runner/prepare-task.js +47 -2745
  86. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  87. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  88. package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
  89. package/dist/core/runner/prepare-wiring-manifest.js +16 -10
  90. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  91. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  92. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  93. package/dist/core/runner/prompt-hash-salt.js +2 -0
  94. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  95. package/dist/core/runner/remote-env-retry.js +16 -0
  96. package/dist/core/runner/runtask.d.ts +16 -31
  97. package/dist/core/runner/runtask.js +110 -121
  98. package/dist/core/runner/terminal-projection.d.ts +22 -0
  99. package/dist/core/runner/terminal-projection.js +28 -0
  100. package/dist/core/session.d.ts +12 -0
  101. package/dist/core/session.js +3 -0
  102. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  103. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  104. package/dist/core/terminal-cause.d.ts +137 -0
  105. package/dist/core/terminal-cause.js +9 -0
  106. package/dist/core/tool-policy.d.ts +43 -139
  107. package/dist/core/tool-policy.js +79 -112
  108. package/dist/core/types.d.ts +67 -164
  109. package/dist/core/wiring-manifest.d.ts +6 -3
  110. package/dist/core/workflow-journal-store.js +3 -4
  111. package/dist/engine/harness/agent-harness.d.ts +1 -1
  112. package/dist/index.d.ts +10 -7
  113. package/dist/index.js +8 -5
  114. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  115. package/dist/orchestration/builtin-workflows.js +1 -1
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.js +5 -3
  118. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  119. package/dist/orchestration/run-workflow-tool.js +4 -4
  120. package/dist/orchestration/workflow-governance.d.ts +4 -4
  121. package/dist/orchestration/workflow-governance.js +4 -2
  122. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  123. package/dist/orchestration/workflow-primitives.js +1 -1
  124. package/dist/orchestration/workflow.d.ts +11 -0
  125. package/dist/orchestration/workflow.js +64 -39
  126. package/dist/prompts/supervisor.d.ts +1 -1
  127. package/dist/prompts/supervisor.js +3 -3
  128. package/dist/scenarios/scenario-registry.js +1 -1
  129. package/package.json +3 -1
  130. package/test/export-surface.snapshot.json +74 -22
  131. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  132. /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, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
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
- const e = new Error(`${ctx.what} on MCP server "${serverLabel}" received no response for ${ctx.idle.idleMs}ms (idle watchdog — ` +
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
- if (err instanceof McpError && err.code === ErrorCode.RequestTimeout) {
357
- const data = err.data;
358
- const totalMs = typeof data?.maxTotalTimeout === "number" ? data.maxTotalTimeout : undefined;
359
- const e = new Error(totalMs !== undefined
360
- ? `${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.)`
361
- : `${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 });
362
- e.errorKind = "timeout";
363
- e.details =
364
- totalMs !== undefined
365
- ? { timedOut: true, timeoutMs: totalMs, totalTimeout: true, server: ctx.server }
366
- : { timedOut: true, timeoutMs: ctx.timeoutMs, server: ctx.server };
367
- finish(e);
368
- }
369
- if (isTransportLost(err)) {
370
- const e = 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 });
371
- e.errorKind = "transport_lost";
372
- e.details = { transportLost: true, server: ctx.server };
373
- finish(e);
374
- }
375
- const httpFailure = describeHttpTransportFailure(err);
376
- if (httpFailure !== undefined) {
377
- const detail = remoteErrorText(err);
378
- const fenced = `\nThe transport error follows as external/untrusted data:\n${fenceServerText(`${ctx.server} transport error`, detail)}`;
379
- const e = new Error(httpFailure.delivered === "no"
380
- ? `${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}`
381
- : `${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 });
382
- e.errorKind = httpFailure.delivered === "no" ? "server_disconnected" : "transport_lost";
383
- e.details = {
384
- server: ctx.server,
385
- ...(httpFailure.delivered === "no" ? {} : { transportLost: true }),
386
- ...(httpFailure.httpStatus !== undefined ? { httpStatus: httpFailure.httpStatus } : {}),
387
- };
388
- finish(e);
389
- }
390
- if (err instanceof McpError) {
391
- const condition = describeMcpSpecErrorCode(err.code);
392
- if (condition !== undefined) {
393
- const e = new Error(`${ctx.what} on MCP server "${serverLabel}" was rejected with MCP protocol error ${err.code} — ${condition}. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, err.message)}`, { cause: err });
394
- e.errorKind = "protocol_error";
395
- e.details = { server: ctx.server, specErrorCode: err.code };
396
- finish(e);
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
- const e = 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.`);
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
- statuses.push({ name: spec.name, status: "failed", errorCode: mcpFailureCodeOf(r.reason, dialKinds[i]), error: namedMcpFailureText(r.reason) });
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
- cacheMcpToolMetadata(h.client, retainedRaw.length > 0 ? [...retainedRaw, ...listed.tools] : listed.tools);
937
- const { serverTools, serverAxes, dropped } = intakeListedTools({ tools: mergedRaw }, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked, h.transportKind);
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
- cacheMcpToolMetadata(client, listed.tools);
1528
- const { serverTools, serverAxes, dropped, advisories } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked, transportKind);
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
+ }