@mcpmesh/sdk 2.4.0 → 2.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 (118) hide show
  1. package/dist/__tests__/agent-single-instance.spec.d.ts +2 -0
  2. package/dist/__tests__/agent-single-instance.spec.d.ts.map +1 -0
  3. package/dist/__tests__/agent-single-instance.spec.js +80 -0
  4. package/dist/__tests__/agent-single-instance.spec.js.map +1 -0
  5. package/dist/__tests__/event-loop-resilience.spec.d.ts +2 -0
  6. package/dist/__tests__/event-loop-resilience.spec.d.ts.map +1 -0
  7. package/dist/__tests__/event-loop-resilience.spec.js +321 -0
  8. package/dist/__tests__/event-loop-resilience.spec.js.map +1 -0
  9. package/dist/__tests__/llm-max-iterations.test.js +10 -8
  10. package/dist/__tests__/llm-max-iterations.test.js.map +1 -1
  11. package/dist/__tests__/llm-provider-multistep.test.d.ts +20 -0
  12. package/dist/__tests__/llm-provider-multistep.test.d.ts.map +1 -0
  13. package/dist/__tests__/llm-provider-multistep.test.js +138 -0
  14. package/dist/__tests__/llm-provider-multistep.test.js.map +1 -0
  15. package/dist/__tests__/llm-provider-output-mode.test.js +1 -0
  16. package/dist/__tests__/llm-provider-output-mode.test.js.map +1 -1
  17. package/dist/__tests__/llm-provider-stopwhen.test.d.ts +22 -0
  18. package/dist/__tests__/llm-provider-stopwhen.test.d.ts.map +1 -0
  19. package/dist/__tests__/llm-provider-stopwhen.test.js +127 -0
  20. package/dist/__tests__/llm-provider-stopwhen.test.js.map +1 -0
  21. package/dist/__tests__/llm-provider-system-synthesis.test.js +1 -0
  22. package/dist/__tests__/llm-provider-system-synthesis.test.js.map +1 -1
  23. package/dist/__tests__/llm-provider-vertex-settings.test.d.ts +18 -0
  24. package/dist/__tests__/llm-provider-vertex-settings.test.d.ts.map +1 -0
  25. package/dist/__tests__/llm-provider-vertex-settings.test.js +128 -0
  26. package/dist/__tests__/llm-provider-vertex-settings.test.js.map +1 -0
  27. package/dist/__tests__/port-conflict-fallback.spec.d.ts +2 -0
  28. package/dist/__tests__/port-conflict-fallback.spec.d.ts.map +1 -0
  29. package/dist/__tests__/port-conflict-fallback.spec.js +123 -0
  30. package/dist/__tests__/port-conflict-fallback.spec.js.map +1 -0
  31. package/dist/__tests__/port-probe-errors.spec.d.ts +2 -0
  32. package/dist/__tests__/port-probe-errors.spec.d.ts.map +1 -0
  33. package/dist/__tests__/port-probe-errors.spec.js +100 -0
  34. package/dist/__tests__/port-probe-errors.spec.js.map +1 -0
  35. package/dist/__tests__/provider-handler-registry.test.d.ts +0 -1
  36. package/dist/__tests__/provider-handler-registry.test.d.ts.map +1 -1
  37. package/dist/__tests__/provider-handler-registry.test.js +23 -1
  38. package/dist/__tests__/provider-handler-registry.test.js.map +1 -1
  39. package/dist/__tests__/proxy-sse-no-data.test.d.ts +13 -0
  40. package/dist/__tests__/proxy-sse-no-data.test.d.ts.map +1 -0
  41. package/dist/__tests__/proxy-sse-no-data.test.js +147 -0
  42. package/dist/__tests__/proxy-sse-no-data.test.js.map +1 -0
  43. package/dist/__tests__/proxy-stream.test.js +26 -0
  44. package/dist/__tests__/proxy-stream.test.js.map +1 -1
  45. package/dist/__tests__/proxy-timer-leak.test.d.ts +16 -0
  46. package/dist/__tests__/proxy-timer-leak.test.d.ts.map +1 -0
  47. package/dist/__tests__/proxy-timer-leak.test.js +97 -0
  48. package/dist/__tests__/proxy-timer-leak.test.js.map +1 -0
  49. package/dist/__tests__/proxy-tool-error.test.d.ts +13 -0
  50. package/dist/__tests__/proxy-tool-error.test.d.ts.map +1 -0
  51. package/dist/__tests__/proxy-tool-error.test.js +313 -0
  52. package/dist/__tests__/proxy-tool-error.test.js.map +1 -0
  53. package/dist/__tests__/route.test.js +21 -1
  54. package/dist/__tests__/route.test.js.map +1 -1
  55. package/dist/__tests__/settle-window.spec.d.ts +2 -0
  56. package/dist/__tests__/settle-window.spec.d.ts.map +1 -0
  57. package/dist/__tests__/settle-window.spec.js +324 -0
  58. package/dist/__tests__/settle-window.spec.js.map +1 -0
  59. package/dist/__tests__/sse.test.js +12 -0
  60. package/dist/__tests__/sse.test.js.map +1 -1
  61. package/dist/__tests__/stop-dispatchers.spec.d.ts +2 -0
  62. package/dist/__tests__/stop-dispatchers.spec.d.ts.map +1 -0
  63. package/dist/__tests__/stop-dispatchers.spec.js +227 -0
  64. package/dist/__tests__/stop-dispatchers.spec.js.map +1 -0
  65. package/dist/agent.d.ts +65 -5
  66. package/dist/agent.d.ts.map +1 -1
  67. package/dist/agent.js +313 -78
  68. package/dist/agent.js.map +1 -1
  69. package/dist/api-runtime.d.ts +33 -3
  70. package/dist/api-runtime.d.ts.map +1 -1
  71. package/dist/api-runtime.js +125 -32
  72. package/dist/api-runtime.js.map +1 -1
  73. package/dist/claim-dispatcher.d.ts +25 -0
  74. package/dist/claim-dispatcher.d.ts.map +1 -1
  75. package/dist/claim-dispatcher.js +59 -1
  76. package/dist/claim-dispatcher.js.map +1 -1
  77. package/dist/config.d.ts +73 -1
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/config.js +108 -2
  80. package/dist/config.js.map +1 -1
  81. package/dist/debug.d.ts +1 -1
  82. package/dist/debug.d.ts.map +1 -1
  83. package/dist/express.d.ts +33 -0
  84. package/dist/express.d.ts.map +1 -1
  85. package/dist/express.js +149 -31
  86. package/dist/express.js.map +1 -1
  87. package/dist/index.d.ts +1 -1
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +1 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/llm-provider.d.ts +18 -0
  92. package/dist/llm-provider.d.ts.map +1 -1
  93. package/dist/llm-provider.js +86 -34
  94. package/dist/llm-provider.js.map +1 -1
  95. package/dist/provider-handlers/gemini-handler.js +6 -0
  96. package/dist/provider-handlers/gemini-handler.js.map +1 -1
  97. package/dist/provider-handlers/provider-handler-registry.d.ts +10 -1
  98. package/dist/provider-handlers/provider-handler-registry.d.ts.map +1 -1
  99. package/dist/provider-handlers/provider-handler-registry.js +4 -1
  100. package/dist/provider-handlers/provider-handler-registry.js.map +1 -1
  101. package/dist/proxy.d.ts.map +1 -1
  102. package/dist/proxy.js +178 -40
  103. package/dist/proxy.js.map +1 -1
  104. package/dist/route.d.ts.map +1 -1
  105. package/dist/route.js +38 -0
  106. package/dist/route.js.map +1 -1
  107. package/dist/settle.d.ts +129 -0
  108. package/dist/settle.d.ts.map +1 -0
  109. package/dist/settle.js +284 -0
  110. package/dist/settle.js.map +1 -0
  111. package/dist/sse.d.ts.map +1 -1
  112. package/dist/sse.js +5 -2
  113. package/dist/sse.js.map +1 -1
  114. package/dist/tracing.d.ts +1 -0
  115. package/dist/tracing.d.ts.map +1 -1
  116. package/dist/tracing.js +3 -0
  117. package/dist/tracing.js.map +1 -1
  118. package/package.json +2 -2
package/dist/agent.d.ts CHANGED
@@ -48,6 +48,13 @@ export declare function __getWorkerToolMap(): Map<string, (...args: unknown[]) =
48
48
  * - Dependency injection for tool functions
49
49
  */
50
50
  export declare class MeshAgent {
51
+ /**
52
+ * Hard cap on the signal-path shutdown sequence. Must exceed the
53
+ * dispatcher drain hard-cap (30s drain + 10s grace, see
54
+ * `stopDispatchers`) so a clean drain isn't cut short; the extra 5s
55
+ * covers pool/A2A teardown and the registry unregister.
56
+ */
57
+ private static readonly SIGNAL_SHUTDOWN_TIMEOUT_MS;
51
58
  private server;
52
59
  private config;
53
60
  private agentId;
@@ -63,6 +70,23 @@ export declare class MeshAgent {
63
70
  private started;
64
71
  private tracingEnabled;
65
72
  private shutdownRequested;
73
+ /**
74
+ * Memoized in-flight (or completed) teardown. `shutdown()` is
75
+ * idempotent: the first caller creates this promise and every later
76
+ * caller — user code, the signal handler, a second signal — awaits
77
+ * the SAME teardown instead of racing a concurrent one (double
78
+ * dispatcher drain, double napi `handle.shutdown()`).
79
+ */
80
+ private shutdownPromise;
81
+ /**
82
+ * This agent's own SIGINT/SIGTERM handler references, kept so
83
+ * shutdown() can `process.off` them. Without removal, sequential
84
+ * MeshAgent instances in one process (legal across async chunk
85
+ * boundaries) accumulate stale handlers whose memoized — already
86
+ * resolved — shutdown() would `process.exit(0)` immediately on the
87
+ * next signal, cutting short the LIVE agent's drain.
88
+ */
89
+ private signalHandlers;
66
90
  /**
67
91
  * Resolved dependencies: composite key -> proxy
68
92
  * Key format: "${toolName}:dep_${depIndex}" (e.g., "myTool:dep_0")
@@ -190,10 +214,15 @@ export declare class MeshAgent {
190
214
  * Install signal handlers for graceful shutdown.
191
215
  * Ensures agent unregisters from registry on SIGINT/SIGTERM.
192
216
  *
193
- * Calls handle.shutdown() directly to trigger Rust core unregistration.
194
- * This causes nextEvent() to return with a "shutdown" event, breaking
195
- * the event loop cleanly. The shutdown is async but we don't await it
196
- * in the signal handler - the event loop handles the exit.
217
+ * Runs the FULL `shutdown()` sequence (issue #1163 MED-2): claim
218
+ * dispatchers drain their in-flight jobs (concurrently, under one
219
+ * shared budget see `stopDispatchers`), A2A clients / HTTP pool /
220
+ * tool-worker pool close, and `handle.shutdown()` unregisters from
221
+ * the registry (which also resolves the event loop's `nextEvent()`
222
+ * with a "shutdown" event, ending it cleanly).
223
+ *
224
+ * The whole sequence is bounded by a force-exit timer so a hang in
225
+ * any cleanup step cannot wedge the process past its SIGTERM grace.
197
226
  */
198
227
  private installSignalHandlers;
199
228
  /**
@@ -202,6 +231,15 @@ export declare class MeshAgent {
202
231
  private startHeartbeat;
203
232
  /**
204
233
  * Run the event loop to handle mesh events.
234
+ *
235
+ * Resilience (issue #1163 MED-1): the loop must outlive individual
236
+ * failures. A throw from an event handler (e.g. a malformed event
237
+ * hitting a non-null assertion) is logged and the loop continues; a
238
+ * `nextEvent()` rejection (e.g. a transient napi failure) backs off
239
+ * exponentially (capped) and retries. Only the "shutdown" event — or
240
+ * the handle being torn down — exits the loop. Previously a single
241
+ * throw broke the loop permanently, freezing dependency-topology
242
+ * updates for the process lifetime while the agent kept serving.
205
243
  */
206
244
  private runEventLoop;
207
245
  /**
@@ -237,6 +275,16 @@ export declare class MeshAgent {
237
275
  * Get all resolved dependencies.
238
276
  */
239
277
  getAllDependencies(): Map<string, McpMeshTool>;
278
+ /**
279
+ * Inject a mock/fake proxy for a capability (the documented mock
280
+ * contract — see `meshctl man testing --typescript`).
281
+ *
282
+ * Fills the dependency slot of every registered tool that declares the
283
+ * capability and marks those slots resolved with the settle state, so
284
+ * the settling-window grace (#1193) never waits on a caller-supplied
285
+ * dependency.
286
+ */
287
+ setMockDependency(capability: string, mock: McpMeshTool): void;
240
288
  /**
241
289
  * Get the agent handle for advanced operations.
242
290
  */
@@ -251,8 +299,20 @@ export declare class MeshAgent {
251
299
  getAgentId(): string;
252
300
  /**
253
301
  * Shutdown the agent gracefully.
302
+ *
303
+ * Idempotent and re-entrant: the first call runs the teardown; every
304
+ * later call (user code, the signal handler, a double signal) returns
305
+ * the SAME promise — later calls' `opts` are ignored. The memo is
306
+ * never cleared: shutdown is terminal, and re-running a half-torn-down
307
+ * cleanup after a failure would be worse than surfacing the original
308
+ * rejection to every caller.
254
309
  */
255
- shutdown(): Promise<void>;
310
+ shutdown(opts?: {
311
+ /** Shared in-flight-handler drain window for claim dispatchers. */
312
+ drainTimeoutMs?: number;
313
+ /** Headroom on top of the drain window before drains are abandoned. */
314
+ drainGraceMs?: number;
315
+ }): Promise<void>;
256
316
  }
257
317
  /**
258
318
  * Create a MeshAgent wrapping a FastMCP server.
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
package/dist/agent.js CHANGED
@@ -11,12 +11,13 @@
11
11
  import { zodToJsonSchema } from "zod-to-json-schema";
12
12
  import { isMainThread } from "node:worker_threads";
13
13
  import { startAgent, } from "@mcpmesh/core";
14
- import { resolveConfig, generateAgentIdSuffix, findAvailablePort } from "./config.js";
14
+ import { resolveConfig, generateAgentIdSuffix, findAvailablePort, resolveStartupBindPort, MAX_CONSECUTIVE_NEXT_EVENT_FAILURES, NEXT_EVENT_BACKOFF_CAP_MS, } from "./config.js";
15
15
  import { enrichSchemaWithMediaTypes } from "./media-param.js";
16
16
  import { createProxy, normalizeDependency, runWithTraceContext, runWithPropagatedHeaders, PROXY_DISPATCH_META } from "./proxy.js";
17
17
  import { readJobHeaders, runWithJobContext, makeJobController, spliceJobController, } from "./inbound-job-dispatch.js";
18
18
  import { MeshJobSubmitter } from "./mesh-job-submitter.js";
19
- import { ClaimDispatcher } from "./claim-dispatcher.js";
19
+ import { getSettleState } from "./settle.js";
20
+ import { ClaimDispatcher, stopDispatchers, } from "./claim-dispatcher.js";
20
21
  import { registerJobHelperTools } from "./jobs-helper-tools.js";
21
22
  import { registerCancelRoute } from "./jobs-cancel-route.js";
22
23
  import { clusterStrictEnabled, normalizeSchemaWithPolicy, } from "./schema-normalize.js";
@@ -68,17 +69,35 @@ const _workerToolMap = new Map();
68
69
  export function __getWorkerToolMap() {
69
70
  return _workerToolMap;
70
71
  }
71
- // Internal: pending agent for auto-start
72
+ // Internal: pending agent for auto-start. The slot holds at most one
73
+ // agent awaiting its auto-start tick; it is consumed (cleared) when the
74
+ // scheduled nextTick fires.
72
75
  let pendingAgent = null;
73
76
  let autoStartScheduled = false;
77
+ // Issue #1163 LOW-6 guard: name of the agent constructed in the CURRENT
78
+ // synchronous chunk. Two constructions in one chunk meant the second
79
+ // silently overwrote `pendingAgent` before the auto-start tick fired —
80
+ // the first agent never started and nobody noticed. The constructor now
81
+ // throws while this guard is set. The guard auto-releases on the first
82
+ // microtask: in real Node the auto-start nextTick drains BEFORE any
83
+ // microtask continuation, so the guard window fully covers the
84
+ // pre-consumption danger zone, while sequential constructions across
85
+ // async boundaries (e.g. one agent per test in a harness) stay allowed.
86
+ let constructionGuardName = null;
74
87
  // Schedule auto-start after module loading completes
75
88
  function scheduleAutoStart() {
76
89
  if (autoStartScheduled)
77
90
  return;
78
91
  autoStartScheduled = true;
79
92
  process.nextTick(() => {
80
- if (pendingAgent) {
81
- pendingAgent._autoStart().catch((err) => {
93
+ // Consume the slot and re-arm the scheduler BEFORE starting, so a
94
+ // later (post-start) construction gets its own auto-start tick
95
+ // instead of being silently dropped.
96
+ const agent = pendingAgent;
97
+ pendingAgent = null;
98
+ autoStartScheduled = false;
99
+ if (agent) {
100
+ agent._autoStart().catch((err) => {
82
101
  console.error("MCP Mesh auto-start failed:", err);
83
102
  process.exit(1);
84
103
  });
@@ -95,6 +114,13 @@ function scheduleAutoStart() {
95
114
  * - Dependency injection for tool functions
96
115
  */
97
116
  export class MeshAgent {
117
+ /**
118
+ * Hard cap on the signal-path shutdown sequence. Must exceed the
119
+ * dispatcher drain hard-cap (30s drain + 10s grace, see
120
+ * `stopDispatchers`) so a clean drain isn't cut short; the extra 5s
121
+ * covers pool/A2A teardown and the registry unregister.
122
+ */
123
+ static SIGNAL_SHUTDOWN_TIMEOUT_MS = 45_000;
98
124
  server;
99
125
  config;
100
126
  agentId;
@@ -110,6 +136,23 @@ export class MeshAgent {
110
136
  started = false;
111
137
  tracingEnabled = false;
112
138
  shutdownRequested = false;
139
+ /**
140
+ * Memoized in-flight (or completed) teardown. `shutdown()` is
141
+ * idempotent: the first caller creates this promise and every later
142
+ * caller — user code, the signal handler, a second signal — awaits
143
+ * the SAME teardown instead of racing a concurrent one (double
144
+ * dispatcher drain, double napi `handle.shutdown()`).
145
+ */
146
+ shutdownPromise = null;
147
+ /**
148
+ * This agent's own SIGINT/SIGTERM handler references, kept so
149
+ * shutdown() can `process.off` them. Without removal, sequential
150
+ * MeshAgent instances in one process (legal across async chunk
151
+ * boundaries) accumulate stale handlers whose memoized — already
152
+ * resolved — shutdown() would `process.exit(0)` immediately on the
153
+ * next signal, cutting short the LIVE agent's drain.
154
+ */
155
+ signalHandlers = null;
113
156
  /**
114
157
  * Resolved dependencies: composite key -> proxy
115
158
  * Key format: "${toolName}:dep_${depIndex}" (e.g., "myTool:dep_0")
@@ -186,7 +229,22 @@ export class MeshAgent {
186
229
  this.config = resolveConfig(config);
187
230
  // Generate unique agent ID with suffix (e.g., "calculator-a1b2c3d4")
188
231
  this.agentId = `${this.config.name}-${generateAgentIdSuffix()}`;
189
- // Register as pending agent for auto-start
232
+ // Register as pending agent for auto-start. Throw if another agent
233
+ // was already constructed in this synchronous chunk — the previous
234
+ // behavior overwrote the slot and the earlier agent silently never
235
+ // started (issue #1163 LOW-6). Only one MeshAgent per process is
236
+ // supported (it owns the FastMCP HTTP server, registry heartbeat,
237
+ // and process-wide signal handlers).
238
+ if (constructionGuardName !== null) {
239
+ throw new Error(`Only one MeshAgent may be constructed per process: agent ` +
240
+ `'${constructionGuardName}' is already pending auto-start. ` +
241
+ `Register all tools on a single MeshAgent instead of constructing ` +
242
+ `'${this.config.name}' as a second agent.`);
243
+ }
244
+ constructionGuardName = this.config.name;
245
+ queueMicrotask(() => {
246
+ constructionGuardName = null;
247
+ });
190
248
  pendingAgent = this;
191
249
  scheduleAutoStart();
192
250
  }
@@ -373,6 +431,16 @@ export class MeshAgent {
373
431
  // Normalize dependencies
374
432
  const normalizedDeps = (def.dependencies ?? []).map(normalizeDependency);
375
433
  const depEndpoints = normalizedDeps.map((d) => d.capability);
434
+ // Settling-window grace (#1193): declare this tool's proxy deps with the
435
+ // process-wide settle state so the agent-level "all declared deps
436
+ // resolved" latch can flip eagerly. The MeshJob slot is excluded — its
437
+ // submitter is constructed locally, not resolved by an event.
438
+ const settleState = getSettleState();
439
+ normalizedDeps.forEach((_dep, depIndex) => {
440
+ if (depIndex !== def.meshJobDepIndex) {
441
+ settleState.registerDeclared(`${toolName}:dep_${depIndex}`);
442
+ }
443
+ });
376
444
  // Capture for closures — these reads must be live at invocation
377
445
  // time (e.g. registryUrl/agentId aren't set yet at addTool time).
378
446
  const isTaskTool = def.task === true;
@@ -403,6 +471,24 @@ export class MeshAgent {
403
471
  }
404
472
  // Create wrapper that injects dependencies positionally and handles tracing
405
473
  const wrappedExecute = async (args) => {
474
+ // Settling-window grace (#1193): while the agent is still settling,
475
+ // wait — bounded by the remaining settle budget — for any declared
476
+ // dep this call would inject that is still unresolved. No-op (single
477
+ // latch check) once settled; the deps array below is built AFTER the
478
+ // wait so it re-reads the resolution state.
479
+ if (normalizedDeps.length > 0 && !settleState.isSettled()) {
480
+ const pendingSettle = [];
481
+ normalizedDeps.forEach((dep, depIndex) => {
482
+ const depKey = `${toolName}:dep_${depIndex}`;
483
+ if (depIndex !== meshJobDepIndex &&
484
+ !this.resolvedDeps.has(depKey)) {
485
+ pendingSettle.push({ depKey, capability: dep.capability });
486
+ }
487
+ });
488
+ if (pendingSettle.length > 0) {
489
+ await settleState.awaitPending(pendingSettle);
490
+ }
491
+ }
406
492
  // Build positional deps array using composite keys (toolName:dep_index)
407
493
  // Phase 1 MeshJob substrate (consumer-side): if meshJobDepIndex is
408
494
  // set, swap the McpMeshTool proxy at that slot for a
@@ -651,6 +737,22 @@ export class MeshAgent {
651
737
  if (isTaskTool) {
652
738
  const capability = def.capability ?? toolName;
653
739
  const handler = async (payload, controller) => {
740
+ // Settling-window grace (#1193): claim dispatch gets the same
741
+ // bounded wait as the inbound HTTP path — a claim arriving during
742
+ // the settling window would otherwise see null deps.
743
+ if (normalizedDeps.length > 0 && !settleState.isSettled()) {
744
+ const pendingSettle = [];
745
+ normalizedDeps.forEach((dep, depIndex) => {
746
+ const depKey = `${toolName}:dep_${depIndex}`;
747
+ if (depIndex !== meshJobDepIndex &&
748
+ !this.resolvedDeps.has(depKey)) {
749
+ pendingSettle.push({ depKey, capability: dep.capability });
750
+ }
751
+ });
752
+ if (pendingSettle.length > 0) {
753
+ await settleState.awaitPending(pendingSettle);
754
+ }
755
+ }
654
756
  const liveDeps = normalizedDeps.map((dep, depIndex) => {
655
757
  if (depIndex === meshJobDepIndex) {
656
758
  return new MeshJobSubmitter(dep.capability, this.agentId, this.config.registryUrl);
@@ -840,11 +942,17 @@ export class MeshAgent {
840
942
  // Auto-detect template base path from agent's package.json location
841
943
  // This ensures file:// templates resolve correctly regardless of cwd
842
944
  findAndSetBasePath();
843
- // Handle httpPort=0: auto-assign an available port
844
- if (this.config.httpPort === 0) {
845
- const assignedPort = await findAvailablePort();
846
- this.config = { ...this.config, httpPort: assignedPort };
847
- console.log(`Auto-assigned port ${assignedPort} for agent`);
945
+ // Resolve the bind port BEFORE tracing/server/heartbeat so every
946
+ // downstream consumer sees the port we will actually bind (issue
947
+ // #1194: a conflict falls back to an OS-assigned port with a
948
+ // prominent warning — see resolveStartupBindPort). The heartbeat
949
+ // reads `this.config.httpPort`, so registration carries the ACTUAL
950
+ // port instead of a phantom endpoint.
951
+ {
952
+ const resolvedPort = await resolveStartupBindPort(this.config.httpPort, "agent");
953
+ if (resolvedPort !== this.config.httpPort) {
954
+ this.config = { ...this.config, httpPort: resolvedPort };
955
+ }
848
956
  }
849
957
  console.log(`Starting MCP Mesh agent: ${this.agentId}`);
850
958
  // Prepare TLS credentials (fetches from Vault if configured)
@@ -1035,38 +1143,59 @@ export class MeshAgent {
1035
1143
  * Install signal handlers for graceful shutdown.
1036
1144
  * Ensures agent unregisters from registry on SIGINT/SIGTERM.
1037
1145
  *
1038
- * Calls handle.shutdown() directly to trigger Rust core unregistration.
1039
- * This causes nextEvent() to return with a "shutdown" event, breaking
1040
- * the event loop cleanly. The shutdown is async but we don't await it
1041
- * in the signal handler - the event loop handles the exit.
1146
+ * Runs the FULL `shutdown()` sequence (issue #1163 MED-2): claim
1147
+ * dispatchers drain their in-flight jobs (concurrently, under one
1148
+ * shared budget see `stopDispatchers`), A2A clients / HTTP pool /
1149
+ * tool-worker pool close, and `handle.shutdown()` unregisters from
1150
+ * the registry (which also resolves the event loop's `nextEvent()`
1151
+ * with a "shutdown" event, ending it cleanly).
1152
+ *
1153
+ * The whole sequence is bounded by a force-exit timer so a hang in
1154
+ * any cleanup step cannot wedge the process past its SIGTERM grace.
1042
1155
  */
1043
1156
  installSignalHandlers() {
1157
+ // Dedupe repeated signals locally. This must NOT key off
1158
+ // `shutdownRequested` (set by shutdown() itself): a signal arriving
1159
+ // while a user-initiated shutdown() is draining must still arm the
1160
+ // force-exit timer and exit the process when that same (memoized)
1161
+ // shutdown completes.
1162
+ let signalHandled = false;
1044
1163
  const shutdownHandler = (signal) => {
1045
- if (this.shutdownRequested)
1164
+ if (signalHandled)
1046
1165
  return;
1047
- this.shutdownRequested = true;
1166
+ signalHandled = true;
1048
1167
  console.log(`\nReceived ${signal}, shutting down agent ${this.agentId}...`);
1049
- // Close HTTPS proxy if it exists
1050
- if (this.httpsProxy) {
1051
- this.httpsProxy.close();
1052
- }
1053
- // Call shutdown directly - this triggers Rust core to unregister
1054
- // and send a shutdown event that breaks the event loop
1055
- if (this.handle) {
1056
- this.handle.shutdown().then(() => {
1057
- console.log(`Agent ${this.agentId} unregistered from registry`);
1058
- process.exit(0);
1059
- }).catch((err) => {
1060
- console.error("Error during shutdown:", err);
1061
- process.exit(1);
1062
- });
1063
- }
1064
- else {
1168
+ // Bounded overall shutdown: the dispatcher drain phase is already
1169
+ // hard-capped (30s drain + 10s grace shared across dispatchers);
1170
+ // this timer covers everything else (pool/A2A close, registry
1171
+ // unregister) so a hang anywhere cannot block exit forever.
1172
+ //
1173
+ // Deliberately ref'd (no unref): both completion paths below
1174
+ // clearTimeout and process.exit synchronously, so the timer never
1175
+ // delays a successful exit — but it must keep a wedged shutdown's
1176
+ // otherwise-empty event loop alive long enough to emit the loud
1177
+ // exit(1) diagnostic instead of silently exiting 0.
1178
+ const forceExitTimer = setTimeout(() => {
1179
+ console.error(`Shutdown did not complete within ${MeshAgent.SIGNAL_SHUTDOWN_TIMEOUT_MS}ms; forcing exit`);
1180
+ process.exit(1);
1181
+ }, MeshAgent.SIGNAL_SHUTDOWN_TIMEOUT_MS);
1182
+ this.shutdown().then(() => {
1183
+ clearTimeout(forceExitTimer);
1184
+ console.log(`Agent ${this.agentId} shut down cleanly`);
1065
1185
  process.exit(0);
1066
- }
1186
+ }).catch((err) => {
1187
+ clearTimeout(forceExitTimer);
1188
+ console.error("Error during shutdown:", err);
1189
+ process.exit(1);
1190
+ });
1067
1191
  };
1068
- process.on("SIGINT", () => shutdownHandler("SIGINT"));
1069
- process.on("SIGTERM", () => shutdownHandler("SIGTERM"));
1192
+ // Keep named references so shutdown() can remove exactly these
1193
+ // listeners (anonymous arrows can't be process.off'd).
1194
+ const sigint = () => shutdownHandler("SIGINT");
1195
+ const sigterm = () => shutdownHandler("SIGTERM");
1196
+ this.signalHandlers = { sigint, sigterm };
1197
+ process.on("SIGINT", sigint);
1198
+ process.on("SIGTERM", sigterm);
1070
1199
  }
1071
1200
  /**
1072
1201
  * Start the Rust core heartbeat loop.
@@ -1242,13 +1371,56 @@ export class MeshAgent {
1242
1371
  }
1243
1372
  /**
1244
1373
  * Run the event loop to handle mesh events.
1374
+ *
1375
+ * Resilience (issue #1163 MED-1): the loop must outlive individual
1376
+ * failures. A throw from an event handler (e.g. a malformed event
1377
+ * hitting a non-null assertion) is logged and the loop continues; a
1378
+ * `nextEvent()` rejection (e.g. a transient napi failure) backs off
1379
+ * exponentially (capped) and retries. Only the "shutdown" event — or
1380
+ * the handle being torn down — exits the loop. Previously a single
1381
+ * throw broke the loop permanently, freezing dependency-topology
1382
+ * updates for the process lifetime while the agent kept serving.
1245
1383
  */
1246
1384
  async runEventLoop() {
1247
1385
  if (!this.handle)
1248
1386
  return;
1387
+ let consecutiveNextEventFailures = 0;
1249
1388
  while (true) {
1389
+ // Handle is nulled by shutdown(); exit cleanly instead of
1390
+ // spinning on a dead reference.
1391
+ if (!this.handle) {
1392
+ console.log("Event loop: handle closed, exiting");
1393
+ return;
1394
+ }
1395
+ let event;
1396
+ try {
1397
+ event = await this.handle.nextEvent();
1398
+ consecutiveNextEventFailures = 0;
1399
+ }
1400
+ catch (err) {
1401
+ // Explicit shutdown() racing a failing nextEvent(): exit
1402
+ // promptly instead of burning more backoff cycles.
1403
+ if (!this.handle || this.shutdownRequested) {
1404
+ console.log("Event loop: shutdown requested, exiting");
1405
+ return;
1406
+ }
1407
+ consecutiveNextEventFailures++;
1408
+ // Ceiling (~60s of continuous failure — see the constant's doc
1409
+ // in config.ts): a permanently broken handle must not retry
1410
+ // forever, keeping the process alive via the backoff timer.
1411
+ if (consecutiveNextEventFailures >= MAX_CONSECUTIVE_NEXT_EVENT_FAILURES) {
1412
+ console.error(`Event loop: terminating after ${consecutiveNextEventFailures} ` +
1413
+ `consecutive nextEvent() failures; dependency topology is ` +
1414
+ `frozen for the remainder of the process:`, err);
1415
+ return;
1416
+ }
1417
+ const backoffMs = Math.min(100 * 2 ** (consecutiveNextEventFailures - 1), NEXT_EVENT_BACKOFF_CAP_MS);
1418
+ console.error(`Event loop: nextEvent() failed (consecutive=${consecutiveNextEventFailures}), ` +
1419
+ `retrying in ${backoffMs}ms:`, err);
1420
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
1421
+ continue;
1422
+ }
1250
1423
  try {
1251
- const event = await this.handle.nextEvent();
1252
1424
  switch (event.eventType) {
1253
1425
  case "agent_registered":
1254
1426
  console.log(`Agent registered with ID: ${event.agentId}`);
@@ -1314,8 +1486,10 @@ export class MeshAgent {
1314
1486
  }
1315
1487
  }
1316
1488
  catch (err) {
1317
- console.error("Event loop error:", err);
1318
- break;
1489
+ // Per-event isolation: a bad event (or a bug in one handler)
1490
+ // must not kill dependency-event processing for the process
1491
+ // lifetime. Log and keep consuming events.
1492
+ console.error(`Event loop: error handling event '${event.eventType}':`, err);
1319
1493
  }
1320
1494
  }
1321
1495
  }
@@ -1335,6 +1509,10 @@ export class MeshAgent {
1335
1509
  const depKey = `${requestingFunction}:dep_${depIndex}`;
1336
1510
  const proxy = createProxy(endpoint, capability, functionName, kwargs);
1337
1511
  this.resolvedDeps.set(depKey, proxy);
1512
+ // Settling-window grace (#1193): wake any settling call waiting on
1513
+ // this dependency AFTER the proxy is stored so the woken call
1514
+ // re-reads a real proxy.
1515
+ getSettleState().markResolved(depKey);
1338
1516
  console.log(`Dependency available: ${capability} at ${endpoint} (tool: ${requestingFunction}, index: ${depIndex}, agent: ${agentId})`);
1339
1517
  return;
1340
1518
  }
@@ -1350,6 +1528,7 @@ export class MeshAgent {
1350
1528
  const depKey = `${toolName}:dep_${idx}`;
1351
1529
  const proxy = createProxy(endpoint, capability, functionName, kwargs);
1352
1530
  this.resolvedDeps.set(depKey, proxy);
1531
+ getSettleState().markResolved(depKey);
1353
1532
  matchCount++;
1354
1533
  }
1355
1534
  });
@@ -1417,6 +1596,30 @@ export class MeshAgent {
1417
1596
  getAllDependencies() {
1418
1597
  return new Map(this.resolvedDeps);
1419
1598
  }
1599
+ /**
1600
+ * Inject a mock/fake proxy for a capability (the documented mock
1601
+ * contract — see `meshctl man testing --typescript`).
1602
+ *
1603
+ * Fills the dependency slot of every registered tool that declares the
1604
+ * capability and marks those slots resolved with the settle state, so
1605
+ * the settling-window grace (#1193) never waits on a caller-supplied
1606
+ * dependency.
1607
+ */
1608
+ setMockDependency(capability, mock) {
1609
+ for (const [toolName, meta] of this.tools.entries()) {
1610
+ if (!meta.dependencies)
1611
+ continue;
1612
+ meta.dependencies.forEach((dep, depIndex) => {
1613
+ if (dep.capability === capability) {
1614
+ const depKey = `${toolName}:dep_${depIndex}`;
1615
+ this.resolvedDeps.set(depKey, mock);
1616
+ // A caller-supplied slot needs no resolution event — count it
1617
+ // as resolved so settling calls never wait on it.
1618
+ getSettleState().markResolved(depKey);
1619
+ }
1620
+ });
1621
+ }
1622
+ }
1420
1623
  /**
1421
1624
  * Get the agent handle for advanced operations.
1422
1625
  */
@@ -1437,50 +1640,82 @@ export class MeshAgent {
1437
1640
  }
1438
1641
  /**
1439
1642
  * Shutdown the agent gracefully.
1643
+ *
1644
+ * Idempotent and re-entrant: the first call runs the teardown; every
1645
+ * later call (user code, the signal handler, a double signal) returns
1646
+ * the SAME promise — later calls' `opts` are ignored. The memo is
1647
+ * never cleared: shutdown is terminal, and re-running a half-torn-down
1648
+ * cleanup after a failure would be worse than surfacing the original
1649
+ * rejection to every caller.
1440
1650
  */
1441
- async shutdown() {
1442
- // Phase 1 MeshJob substrate: stop claim dispatchers first so
1443
- // they don't pull a fresh job mid-shutdown.
1444
- for (const d of this._claimDispatchers) {
1651
+ shutdown(opts) {
1652
+ if (this.shutdownPromise)
1653
+ return this.shutdownPromise;
1654
+ this.shutdownRequested = true;
1655
+ this.shutdownPromise = (async () => {
1656
+ // Phase 1 MeshJob substrate: stop claim dispatchers first so they
1657
+ // don't pull a fresh job mid-shutdown. Issue #1173: all dispatchers
1658
+ // drain CONCURRENTLY under one shared, hard-capped budget — never
1659
+ // N×30s sequential — and a hanging drain is abandoned with a
1660
+ // warning so the registry unregister below always runs.
1661
+ await stopDispatchers(this._claimDispatchers, opts?.drainTimeoutMs, opts?.drainGraceMs);
1662
+ this._claimDispatchers = [];
1663
+ // Issue #917: mark all cached A2AClients closed so any in-flight
1664
+ // user code raises cleanly instead of reusing a torn-down instance.
1665
+ // Close in parallel so one slow client doesn't block the others —
1666
+ // the undici Agent pool is shared via closeHttpPool() below.
1667
+ const closePromises = Array.from(this._a2aClients.values()).map((client) => client.close().catch((err) => {
1668
+ console.warn("[mesh-a2a] Error closing A2AClient:", err);
1669
+ return null;
1670
+ }));
1671
+ await Promise.allSettled(closePromises);
1672
+ this._a2aClients.clear();
1445
1673
  try {
1446
- await d.stop();
1674
+ await closeHttpPool();
1447
1675
  }
1448
1676
  catch (err) {
1449
- console.warn(`[mesh-jobs] error stopping claim dispatcher:`, err);
1677
+ console.warn("Error closing HTTP pool:", err);
1450
1678
  }
1451
- }
1452
- this._claimDispatchers = [];
1453
- // Issue #917: mark all cached A2AClients closed so any in-flight
1454
- // user code raises cleanly instead of reusing a torn-down instance.
1455
- // Close in parallel so one slow client doesn't block the others —
1456
- // the undici Agent pool is shared via closeHttpPool() below.
1457
- const closePromises = Array.from(this._a2aClients.values()).map((client) => client.close().catch((err) => {
1458
- console.warn("[mesh-a2a] Error closing A2AClient:", err);
1459
- return null;
1460
- }));
1461
- await Promise.allSettled(closePromises);
1462
- this._a2aClients.clear();
1463
- try {
1464
- await closeHttpPool();
1465
- }
1466
- catch (err) {
1467
- console.warn("Error closing HTTP pool:", err);
1468
- }
1469
- try {
1470
- await closePool();
1471
- }
1472
- catch (err) {
1473
- console.warn("Error closing tool worker pool:", err);
1474
- }
1475
- if (this.httpsProxy) {
1476
- this.httpsProxy.close();
1477
- this.httpsProxy = undefined;
1478
- }
1479
- if (this.handle) {
1480
- await this.handle.shutdown();
1481
- this.handle = null;
1482
- }
1483
- cleanupTls();
1679
+ try {
1680
+ await closePool();
1681
+ }
1682
+ catch (err) {
1683
+ console.warn("Error closing tool worker pool:", err);
1684
+ }
1685
+ if (this.httpsProxy) {
1686
+ // Await the close callback so the TLS listener's port is
1687
+ // actually released by the time shutdown() resolves (mirrors
1688
+ // MeshExpress.shutdown()'s server.close handling).
1689
+ const proxy = this.httpsProxy;
1690
+ this.httpsProxy = undefined;
1691
+ await new Promise((resolve) => {
1692
+ proxy.close((err) => {
1693
+ if (err)
1694
+ console.warn("Error closing HTTPS proxy:", err);
1695
+ resolve();
1696
+ });
1697
+ });
1698
+ }
1699
+ // Registry unregister runs regardless of how the cleanup steps above
1700
+ // fared — every prior step is guarded so a drain/close failure can't
1701
+ // leave a stale registration behind.
1702
+ if (this.handle) {
1703
+ await this.handle.shutdown();
1704
+ this.handle = null;
1705
+ }
1706
+ cleanupTls();
1707
+ // Remove this agent's own signal listeners LAST: during the drain
1708
+ // above a signal must still reach the handler (it arms the
1709
+ // force-exit timer and exits when this memoized teardown settles).
1710
+ // Leaving them installed would let a LATER agent's signal hit this
1711
+ // already-resolved shutdown() and process.exit(0) prematurely.
1712
+ if (this.signalHandlers) {
1713
+ process.off("SIGINT", this.signalHandlers.sigint);
1714
+ process.off("SIGTERM", this.signalHandlers.sigterm);
1715
+ this.signalHandlers = null;
1716
+ }
1717
+ })();
1718
+ return this.shutdownPromise;
1484
1719
  }
1485
1720
  }
1486
1721
  /**