comfyui-node 1.4.4 → 1.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 (58) hide show
  1. package/README.md +21 -16
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/call-wrapper.d.ts +141 -124
  4. package/dist/call-wrapper.d.ts.map +1 -1
  5. package/dist/call-wrapper.js +353 -64
  6. package/dist/call-wrapper.js.map +1 -1
  7. package/dist/client.d.ts +290 -290
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +78 -19
  10. package/dist/client.js.map +1 -1
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/pool/SmartPool.d.ts +144 -0
  16. package/dist/pool/SmartPool.d.ts.map +1 -0
  17. package/dist/pool/SmartPool.js +677 -0
  18. package/dist/pool/SmartPool.js.map +1 -0
  19. package/dist/pool/SmartPoolV2.d.ts +120 -0
  20. package/dist/pool/SmartPoolV2.d.ts.map +1 -0
  21. package/dist/pool/SmartPoolV2.js +587 -0
  22. package/dist/pool/SmartPoolV2.js.map +1 -0
  23. package/dist/pool/WorkflowPool.d.ts +32 -2
  24. package/dist/pool/WorkflowPool.d.ts.map +1 -1
  25. package/dist/pool/WorkflowPool.js +298 -66
  26. package/dist/pool/WorkflowPool.js.map +1 -1
  27. package/dist/pool/client/ClientManager.d.ts +4 -2
  28. package/dist/pool/client/ClientManager.d.ts.map +1 -1
  29. package/dist/pool/client/ClientManager.js +29 -9
  30. package/dist/pool/client/ClientManager.js.map +1 -1
  31. package/dist/pool/index.d.ts +2 -0
  32. package/dist/pool/index.d.ts.map +1 -1
  33. package/dist/pool/index.js +2 -0
  34. package/dist/pool/index.js.map +1 -1
  35. package/dist/pool/queue/QueueAdapter.d.ts +32 -30
  36. package/dist/pool/queue/QueueAdapter.d.ts.map +1 -1
  37. package/dist/pool/queue/adapters/memory.d.ts +22 -20
  38. package/dist/pool/queue/adapters/memory.d.ts.map +1 -1
  39. package/dist/pool/queue/adapters/memory.js +14 -2
  40. package/dist/pool/queue/adapters/memory.js.map +1 -1
  41. package/dist/pool/types/affinity.d.ts +6 -0
  42. package/dist/pool/types/affinity.d.ts.map +1 -0
  43. package/dist/pool/types/affinity.js +2 -0
  44. package/dist/pool/types/affinity.js.map +1 -0
  45. package/dist/pool/types/job.d.ts.map +1 -1
  46. package/dist/pool/utils/failure-analysis.d.ts +14 -0
  47. package/dist/pool/utils/failure-analysis.d.ts.map +1 -0
  48. package/dist/pool/utils/failure-analysis.js +224 -0
  49. package/dist/pool/utils/failure-analysis.js.map +1 -0
  50. package/dist/pool.d.ts +180 -180
  51. package/dist/types/error.d.ts +31 -1
  52. package/dist/types/error.d.ts.map +1 -1
  53. package/dist/types/error.js +30 -0
  54. package/dist/types/error.js.map +1 -1
  55. package/dist/workflow.d.ts.map +1 -1
  56. package/dist/workflow.js +4 -1
  57. package/dist/workflow.js.map +1 -1
  58. package/package.json +4 -3
@@ -1,5 +1,7 @@
1
1
  import { FailedCacheError, WentMissingError, EnqueueFailedError, DisconnectedError, CustomEventError, ExecutionFailedError, ExecutionInterruptedError, MissingNodeError } from "./types/error.js";
2
2
  import { buildEnqueueFailedError } from "./utils/response-error.js";
3
+ const DISCONNECT_FAILURE_GRACE_MS = 5000;
4
+ const CALL_WRAPPER_DEBUG = process.env.WORKFLOW_POOL_DEBUG === "1";
3
5
  /**
4
6
  * Represents a wrapper class for making API calls using the ComfyApi client.
5
7
  * Provides methods for setting callback functions and executing the job.
@@ -19,6 +21,15 @@ export class CallWrapper {
19
21
  onFinishedFn;
20
22
  onFailedFn;
21
23
  onProgressFn;
24
+ jobResolveFn;
25
+ jobDoneResolved = false;
26
+ pendingCompletion = null;
27
+ cancellationRequested = false;
28
+ promptLoadTrigger = null;
29
+ disconnectRecoveryActive = false;
30
+ disconnectFailureTimer = null;
31
+ onReconnectHandlerOffFn;
32
+ onReconnectFailedHandlerOffFn;
22
33
  onDisconnectedHandlerOffFn;
23
34
  checkExecutingOffFn;
24
35
  checkExecutedOffFn;
@@ -133,19 +144,39 @@ export class CallWrapper {
133
144
  * Start the job execution.
134
145
  */
135
146
  this.emitLog("CallWrapper.run", "enqueue start");
147
+ this.pendingCompletion = null;
148
+ this.jobResolveFn = undefined;
149
+ this.jobDoneResolved = false;
150
+ this.cancellationRequested = false;
151
+ this.promptLoadTrigger = null;
136
152
  const job = await this.enqueueJob();
137
153
  if (!job) {
138
154
  // enqueueJob already invoked onFailed with a rich error instance; just abort.
139
155
  this.emitLog("CallWrapper.run", "enqueue failed -> abort");
140
156
  return false;
141
157
  }
142
- let promptLoadTrigger;
143
158
  const promptLoadCached = new Promise((resolve) => {
144
- promptLoadTrigger = resolve;
159
+ this.promptLoadTrigger = (value) => {
160
+ if (this.promptLoadTrigger) {
161
+ this.promptLoadTrigger = null;
162
+ }
163
+ resolve(value);
164
+ };
145
165
  });
146
- let jobDoneTrigger;
147
166
  const jobDonePromise = new Promise((resolve) => {
148
- jobDoneTrigger = resolve;
167
+ this.jobDoneResolved = false;
168
+ this.jobResolveFn = (value) => {
169
+ if (this.jobDoneResolved) {
170
+ return;
171
+ }
172
+ this.jobDoneResolved = true;
173
+ resolve(value);
174
+ };
175
+ if (this.pendingCompletion !== null) {
176
+ const pending = this.pendingCompletion;
177
+ this.pendingCompletion = null;
178
+ this.jobResolveFn?.(pending);
179
+ }
149
180
  });
150
181
  /**
151
182
  * Declare the function to check if the job is executing.
@@ -153,7 +184,7 @@ export class CallWrapper {
153
184
  const checkExecutingFn = (event) => {
154
185
  if (event.detail && event.detail.prompt_id === job.prompt_id) {
155
186
  this.emitLog("CallWrapper.run", "executing observed", { node: event.detail.node });
156
- promptLoadTrigger(false);
187
+ this.resolvePromptLoad(false);
157
188
  }
158
189
  };
159
190
  /**
@@ -171,7 +202,7 @@ export class CallWrapper {
171
202
  nodes: event.detail.nodes,
172
203
  expected: outputNodes
173
204
  });
174
- promptLoadTrigger(cached);
205
+ this.resolvePromptLoad(cached);
175
206
  }
176
207
  };
177
208
  /**
@@ -200,6 +231,15 @@ export class CallWrapper {
200
231
  this.emitLog("CallWrapper.status", "cached output already handled");
201
232
  return;
202
233
  }
234
+ if (this.cancellationRequested) {
235
+ this.emitLog("CallWrapper.status", "job missing after cancellation", {
236
+ prompt_id: job.prompt_id
237
+ });
238
+ this.resolvePromptLoad(false);
239
+ this.resolveJob(false);
240
+ this.cleanupListeners("status handler cancellation");
241
+ return;
242
+ }
203
243
  wentMissing = true;
204
244
  const output = await this.handleCachedOutput(job.prompt_id);
205
245
  if (output) {
@@ -207,22 +247,31 @@ export class CallWrapper {
207
247
  this.emitLog("CallWrapper.status", "output from history after missing", {
208
248
  prompt_id: job.prompt_id
209
249
  });
210
- jobDoneTrigger(output);
250
+ this.resolvePromptLoad(false);
251
+ this.resolveJob(output);
211
252
  this.cleanupListeners("status handler resolved from history");
212
253
  return;
213
254
  }
255
+ if (this.disconnectRecoveryActive) {
256
+ this.emitLog("CallWrapper.status", "job missing but disconnect recovery active -> waiting", {
257
+ prompt_id: job.prompt_id
258
+ });
259
+ this.resolvePromptLoad(false);
260
+ void this.attemptHistoryCompletion("status_missing");
261
+ return;
262
+ }
214
263
  cachedOutputDone = true;
215
264
  this.emitLog("CallWrapper.status", "job missing without cached output", {
216
265
  prompt_id: job.prompt_id
217
266
  });
218
- promptLoadTrigger(false);
219
- jobDoneTrigger(false);
267
+ this.resolvePromptLoad(false);
268
+ this.resolveJob(false);
220
269
  this.cleanupListeners("status handler missing");
221
- this.onFailedFn?.(new WentMissingError("The job went missing!"), job.prompt_id);
270
+ this.emitFailure(new WentMissingError("The job went missing!"), job.prompt_id);
222
271
  };
223
272
  this.statusHandlerOffFn = this.client.on("status", statusHandler);
224
273
  // Attach execution listeners immediately so fast jobs cannot finish before we subscribe
225
- this.handleJobExecution(job.prompt_id, jobDoneTrigger);
274
+ this.handleJobExecution(job.prompt_id);
226
275
  await promptLoadCached;
227
276
  if (wentMissing) {
228
277
  return jobDonePromise;
@@ -232,14 +281,14 @@ export class CallWrapper {
232
281
  if (output) {
233
282
  cachedOutputDone = true;
234
283
  this.cleanupListeners("no cached output values returned");
235
- jobDoneTrigger(output);
284
+ this.resolveJob(output);
236
285
  return output;
237
286
  }
238
287
  if (output === false) {
239
288
  cachedOutputDone = true;
240
289
  this.cleanupListeners("cached output ready before execution listeners");
241
- this.onFailedFn?.(new FailedCacheError("Failed to get cached output"), this.promptId);
242
- jobDoneTrigger(false);
290
+ this.emitFailure(new FailedCacheError("Failed to get cached output"), this.promptId);
291
+ this.resolveJob(false);
243
292
  return false;
244
293
  }
245
294
  this.emitLog("CallWrapper.run", "no cached output -> proceed with execution listeners");
@@ -305,10 +354,10 @@ export class CallWrapper {
305
354
  }
306
355
  catch (e) {
307
356
  if (e instanceof Response) {
308
- this.onFailedFn?.(new MissingNodeError("Failed to get workflow node definitions", { cause: await e.json() }));
357
+ this.emitFailure(new MissingNodeError("Failed to get workflow node definitions", { cause: await e.json() }), this.promptId);
309
358
  }
310
359
  else {
311
- this.onFailedFn?.(new MissingNodeError("There was a missing node in the workflow bypass.", { cause: e }));
360
+ this.emitFailure(new MissingNodeError("There was a missing node in the workflow bypass.", { cause: e }), this.promptId);
312
361
  }
313
362
  return null;
314
363
  }
@@ -320,22 +369,22 @@ export class CallWrapper {
320
369
  catch (e) {
321
370
  try {
322
371
  if (e instanceof EnqueueFailedError) {
323
- this.onFailedFn?.(e);
372
+ this.emitFailure(e, this.promptId);
324
373
  }
325
374
  else if (e instanceof Response) {
326
375
  const err = await buildEnqueueFailedError(e);
327
- this.onFailedFn?.(err);
376
+ this.emitFailure(err, this.promptId);
328
377
  }
329
378
  else if (e && typeof e === "object" && "response" in e && e.response instanceof Response) {
330
379
  const err = await buildEnqueueFailedError(e.response);
331
- this.onFailedFn?.(err);
380
+ this.emitFailure(err, this.promptId);
332
381
  }
333
382
  else {
334
- this.onFailedFn?.(new EnqueueFailedError("Failed to queue prompt", { cause: e, reason: e?.message }));
383
+ this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: e, reason: e?.message }), this.promptId);
335
384
  }
336
385
  }
337
386
  catch (inner) {
338
- this.onFailedFn?.(new EnqueueFailedError("Failed to queue prompt", { cause: inner }));
387
+ this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: inner }), this.promptId);
339
388
  }
340
389
  job = null;
341
390
  }
@@ -343,20 +392,162 @@ export class CallWrapper {
343
392
  return;
344
393
  }
345
394
  this.promptId = job.prompt_id;
395
+ console.log(`[CallWrapper] Enqueued with promptId=${this.promptId?.substring(0, 8)}...`);
396
+ console.log(`[CallWrapper] Full job object:`, JSON.stringify({ promptId: job.prompt_id }, null, 2));
346
397
  this.emitLog("CallWrapper.enqueueJob", "queued", { prompt_id: this.promptId });
347
398
  this.onPendingFn?.(this.promptId);
348
399
  this.onDisconnectedHandlerOffFn = this.client.on("disconnected", () => {
349
- // Ignore disconnection if we are already successfully completing
350
- // This prevents a race condition where outputs are collected successfully
351
- // but the WebSocket disconnects before cleanupListeners() is called
352
400
  if (this.isCompletingSuccessfully) {
353
401
  this.emitLog("CallWrapper.enqueueJob", "disconnected during success completion -> ignored");
354
402
  return;
355
403
  }
356
- this.onFailedFn?.(new DisconnectedError("Disconnected"), this.promptId);
404
+ this.emitLog("CallWrapper.enqueueJob", "socket disconnected -> enter recovery", { promptId: this.promptId });
405
+ this.startDisconnectRecovery();
406
+ });
407
+ this.onReconnectHandlerOffFn = this.client.on("reconnected", () => {
408
+ if (!this.disconnectRecoveryActive) {
409
+ return;
410
+ }
411
+ this.emitLog("CallWrapper.enqueueJob", "socket reconnected", { promptId: this.promptId });
412
+ this.stopDisconnectRecovery();
413
+ void this.attemptHistoryCompletion("reconnected");
414
+ });
415
+ this.onReconnectFailedHandlerOffFn = this.client.on("reconnection_failed", () => {
416
+ if (!this.disconnectRecoveryActive) {
417
+ return;
418
+ }
419
+ this.emitLog("CallWrapper.enqueueJob", "reconnection failed", { promptId: this.promptId });
420
+ this.failDisconnected("reconnection_failed");
357
421
  });
358
422
  return job;
359
423
  }
424
+ resolvePromptLoad(value) {
425
+ const trigger = this.promptLoadTrigger;
426
+ if (!trigger) {
427
+ return;
428
+ }
429
+ this.promptLoadTrigger = null;
430
+ try {
431
+ trigger(value);
432
+ }
433
+ catch (error) {
434
+ this.emitLog("CallWrapper.resolvePromptLoad", "prompt load trigger threw", {
435
+ error: error instanceof Error ? error.message : String(error),
436
+ promptId: this.promptId
437
+ });
438
+ }
439
+ }
440
+ resolveJob(value) {
441
+ if (CALL_WRAPPER_DEBUG) {
442
+ console.log("[debug] resolveJob", this.promptId, value, Boolean(this.jobResolveFn), this.jobDoneResolved);
443
+ }
444
+ if (this.jobResolveFn) {
445
+ if (this.jobDoneResolved) {
446
+ return;
447
+ }
448
+ this.jobDoneResolved = true;
449
+ this.jobResolveFn(value);
450
+ if (CALL_WRAPPER_DEBUG) {
451
+ console.log("[debug] jobResolveFn invoked", this.promptId);
452
+ }
453
+ }
454
+ else {
455
+ this.pendingCompletion = value;
456
+ }
457
+ }
458
+ emitFailure(error, promptId) {
459
+ const fn = this.onFailedFn;
460
+ if (!fn) {
461
+ return;
462
+ }
463
+ const targetPromptId = promptId ?? this.promptId;
464
+ try {
465
+ if (CALL_WRAPPER_DEBUG) {
466
+ console.log("[debug] emitFailure start", error.name);
467
+ }
468
+ fn(error, targetPromptId);
469
+ if (CALL_WRAPPER_DEBUG) {
470
+ console.log("[debug] emitFailure end", error.name);
471
+ }
472
+ }
473
+ catch (callbackError) {
474
+ this.emitLog("CallWrapper.emitFailure", "onFailed callback threw", {
475
+ prompt_id: targetPromptId,
476
+ error: callbackError instanceof Error ? callbackError.message : String(callbackError)
477
+ });
478
+ }
479
+ }
480
+ cancel(reason = "cancelled") {
481
+ if (this.cancellationRequested) {
482
+ this.emitLog("CallWrapper.cancel", "cancel already requested", {
483
+ promptId: this.promptId,
484
+ reason
485
+ });
486
+ return;
487
+ }
488
+ this.cancellationRequested = true;
489
+ this.emitLog("CallWrapper.cancel", "cancel requested", {
490
+ promptId: this.promptId,
491
+ reason
492
+ });
493
+ this.resolvePromptLoad(false);
494
+ this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: { reason } }), this.promptId);
495
+ this.cleanupListeners("cancel requested");
496
+ this.resolveJob(false);
497
+ }
498
+ startDisconnectRecovery() {
499
+ if (this.disconnectRecoveryActive || this.cancellationRequested) {
500
+ return;
501
+ }
502
+ this.disconnectRecoveryActive = true;
503
+ if (this.disconnectFailureTimer) {
504
+ clearTimeout(this.disconnectFailureTimer);
505
+ }
506
+ this.disconnectFailureTimer = setTimeout(() => this.failDisconnected("timeout"), DISCONNECT_FAILURE_GRACE_MS);
507
+ void this.attemptHistoryCompletion("disconnect_start");
508
+ }
509
+ stopDisconnectRecovery() {
510
+ if (!this.disconnectRecoveryActive) {
511
+ return;
512
+ }
513
+ this.disconnectRecoveryActive = false;
514
+ if (this.disconnectFailureTimer) {
515
+ clearTimeout(this.disconnectFailureTimer);
516
+ this.disconnectFailureTimer = null;
517
+ }
518
+ }
519
+ async attemptHistoryCompletion(reason) {
520
+ if (!this.promptId || this.cancellationRequested) {
521
+ return false;
522
+ }
523
+ try {
524
+ const output = await this.handleCachedOutput(this.promptId);
525
+ if (output && output !== false) {
526
+ this.emitLog("CallWrapper.historyRecovery", "completed from history", { reason, promptId: this.promptId });
527
+ this.stopDisconnectRecovery();
528
+ this.isCompletingSuccessfully = true;
529
+ this.resolvePromptLoad(false);
530
+ this.resolveJob(output);
531
+ this.cleanupListeners(`history recovery (${reason})`);
532
+ return true;
533
+ }
534
+ }
535
+ catch (error) {
536
+ this.emitLog("CallWrapper.historyRecovery", "history fetch failed", { reason, error: String(error) });
537
+ }
538
+ return false;
539
+ }
540
+ failDisconnected(reason) {
541
+ if (!this.disconnectRecoveryActive || this.isCompletingSuccessfully) {
542
+ return;
543
+ }
544
+ this.stopDisconnectRecovery();
545
+ this.emitLog("CallWrapper.enqueueJob", "disconnect recovery failed", { reason, promptId: this.promptId });
546
+ this.resolvePromptLoad(false);
547
+ this.resolveJob(false);
548
+ this.cleanupListeners("disconnect failure");
549
+ this.emitFailure(new DisconnectedError("Disconnected"), this.promptId);
550
+ }
360
551
  async handleCachedOutput(promptId) {
361
552
  const hisData = await this.client.ext.history.getHistory(promptId);
362
553
  this.emitLog("CallWrapper.handleCachedOutput", "history fetched", {
@@ -418,15 +609,31 @@ export class CallWrapper {
418
609
  }
419
610
  return output;
420
611
  }
421
- handleJobExecution(promptId, jobDoneTrigger) {
612
+ handleJobExecution(promptId) {
422
613
  if (this.executionHandlerOffFn) {
423
614
  return;
424
615
  }
425
616
  const reverseOutputMapped = this.reverseMapOutputKeys();
617
+ const mapOutputKeys = this.prompt.mapOutputKeys;
618
+ console.log(`[CallWrapper] handleJobExecution for ${promptId.substring(0, 8)}... - mapOutputKeys:`, mapOutputKeys, "reverseOutputMapped:", reverseOutputMapped);
426
619
  this.progressHandlerOffFn = this.client.on("progress", (ev) => this.handleProgress(ev, promptId));
427
- this.previewHandlerOffFn = this.client.on("b_preview", (ev) => this.onPreviewFn?.(ev.detail, this.promptId));
620
+ this.previewHandlerOffFn = this.client.on("b_preview", (ev) => {
621
+ // Note: b_preview events don't include prompt_id. They're scoped per connection.
622
+ // If multiple jobs use the same connection, they will all receive preview events.
623
+ // This is a limitation of the ComfyUI protocol - previews are not separated by prompt_id.
624
+ this.onPreviewFn?.(ev.detail, this.promptId);
625
+ });
428
626
  // Also forward preview with metadata if available
429
- const offPreviewMeta = this.client.on("b_preview_meta", (ev) => this.onPreviewMetaFn?.(ev.detail, this.promptId));
627
+ const offPreviewMeta = this.client.on("b_preview_meta", (ev) => {
628
+ // Validate prompt_id from metadata if available to prevent cross-user preview leakage
629
+ const metadata = ev.detail.metadata;
630
+ const metaPromptId = metadata?.prompt_id;
631
+ if (metaPromptId && metaPromptId !== promptId) {
632
+ console.log(`[CallWrapper] Ignoring b_preview_meta for wrong prompt. Expected ${promptId.substring(0, 8)}..., got ${metaPromptId.substring(0, 8)}...`);
633
+ return;
634
+ }
635
+ this.onPreviewMetaFn?.(ev.detail, this.promptId);
636
+ });
430
637
  const prevCleanup = this.previewHandlerOffFn;
431
638
  this.previewHandlerOffFn = () => {
432
639
  prevCleanup?.();
@@ -434,16 +641,18 @@ export class CallWrapper {
434
641
  };
435
642
  const totalOutput = Object.keys(reverseOutputMapped).length;
436
643
  let remainingOutput = totalOutput;
644
+ console.log(`[CallWrapper] totalOutput=${totalOutput}, remainingOutput=${remainingOutput}`);
437
645
  const executionHandler = (ev) => {
438
- if (ev.detail.prompt_id !== promptId)
646
+ console.log(`[CallWrapper.executionHandler] received executed event for promptId=${ev.detail.prompt_id?.substring(0, 8)}..., node=${ev.detail.node}, waitingFor=${promptId.substring(0, 8)}...`);
647
+ const eventPromptId = ev.detail.prompt_id;
648
+ const isCorrectPrompt = eventPromptId === promptId;
649
+ // STRICT: Only accept events where prompt_id matches our expected promptId
650
+ if (!isCorrectPrompt) {
651
+ console.log(`[CallWrapper.executionHandler] REJECTED - prompt_id mismatch (expected ${promptId.substring(0, 8)}..., got ${eventPromptId?.substring(0, 8)}...)`);
439
652
  return;
653
+ }
440
654
  const outputKey = reverseOutputMapped[ev.detail.node];
441
- this.emitLog("CallWrapper.executionHandler", "executed event received", {
442
- node: ev.detail.node,
443
- outputKey,
444
- remainingBefore: remainingOutput,
445
- isTrackedOutput: !!outputKey
446
- });
655
+ console.log(`[CallWrapper] executionHandler - promptId: ${promptId.substring(0, 8)}... (event says: ${ev.detail.prompt_id?.substring(0, 8)}...), node: ${ev.detail.node}, outputKey: ${outputKey}, output:`, JSON.stringify(ev.detail.output));
447
656
  if (outputKey) {
448
657
  this.output[outputKey] = ev.detail.output;
449
658
  this.onOutputFn?.(outputKey, ev.detail.output, this.promptId);
@@ -454,54 +663,117 @@ export class CallWrapper {
454
663
  this.output._raw[ev.detail.node] = ev.detail.output;
455
664
  this.onOutputFn?.(ev.detail.node, ev.detail.output, this.promptId);
456
665
  }
457
- this.emitLog("CallWrapper.executionHandler", "after processing executed event", {
458
- remainingAfter: remainingOutput,
459
- willTriggerCompletion: remainingOutput === 0
460
- });
666
+ console.log(`[CallWrapper] afterProcessing - remainingAfter: ${remainingOutput}, willTriggerCompletion: ${remainingOutput === 0}`);
461
667
  if (remainingOutput === 0) {
462
- this.emitLog("CallWrapper.handleJobExecution", "all outputs collected");
668
+ console.log(`[CallWrapper] all outputs collected for ${promptId.substring(0, 8)}...`);
463
669
  // Mark as successfully completing BEFORE cleanup to prevent race condition with disconnection handler
464
670
  this.isCompletingSuccessfully = true;
465
671
  this.cleanupListeners("all outputs collected");
466
672
  this.onFinishedFn?.(this.output, this.promptId);
467
- jobDoneTrigger(this.output);
673
+ this.resolveJob(this.output);
468
674
  }
469
675
  };
470
676
  const executedEnd = async () => {
471
- this.emitLog("CallWrapper.executedEnd", "execution_success fired", {
472
- promptId,
473
- remainingOutput,
474
- totalOutput
475
- });
677
+ console.log(`[CallWrapper] execution_success fired for ${promptId.substring(0, 8)}..., remainingOutput=${remainingOutput}, totalOutput=${totalOutput}`);
678
+ // If we've already marked this as successfully completing, don't fail it again
679
+ if (this.isCompletingSuccessfully) {
680
+ console.log(`[CallWrapper] Already marked as successfully completing, ignoring this execution_success`);
681
+ return;
682
+ }
683
+ if (remainingOutput === 0) {
684
+ console.log(`[CallWrapper] all outputs already collected, nothing to do`);
685
+ return;
686
+ }
687
+ // Wait briefly for outputs that might be arriving due to prompt ID mismatch
688
+ await new Promise(resolve => setTimeout(resolve, 100));
689
+ console.log(`[CallWrapper] After wait - remainingOutput=${remainingOutput}, this.output keys:`, Object.keys(this.output));
690
+ // Check if outputs arrived while we were waiting
476
691
  if (remainingOutput === 0) {
477
- this.emitLog("CallWrapper.executedEnd", "all outputs already collected, nothing to do");
692
+ console.log(`[CallWrapper] Outputs arrived during wait - marking as complete`);
693
+ this.isCompletingSuccessfully = true;
694
+ this.cleanupListeners("executedEnd - outputs complete after wait");
695
+ this.onFinishedFn?.(this.output, this.promptId);
696
+ this.resolveJob(this.output);
697
+ return;
698
+ }
699
+ // Check if we have collected all outputs (even if prompt ID mismatch)
700
+ const hasAllOutputs = Object.keys(reverseOutputMapped).every(nodeId => this.output[reverseOutputMapped[nodeId]] !== undefined);
701
+ if (hasAllOutputs) {
702
+ console.log(`[CallWrapper] Have all required outputs despite promptId mismatch - marking as complete`);
703
+ this.isCompletingSuccessfully = true;
704
+ this.cleanupListeners("executedEnd - outputs complete despite promptId mismatch");
705
+ this.onFinishedFn?.(this.output, this.promptId);
706
+ this.resolveJob(this.output);
478
707
  return;
479
708
  }
480
- const hisData = await this.client.ext.history.getHistory(promptId);
481
- if (hisData?.status?.completed) {
482
- const outputCount = Object.keys(hisData.outputs ?? {}).length;
483
- if (outputCount > 0 && outputCount - totalOutput === 0) {
484
- this.emitLog("CallWrapper.executedEnd", "outputs equal total after history check -> ignore false end");
709
+ // Try to fetch from history with retry logic
710
+ let hisData = null;
711
+ for (let retries = 0; retries < 5; retries++) {
712
+ hisData = await this.client.ext.history.getHistory(promptId);
713
+ console.log(`[CallWrapper] History query result for ${promptId.substring(0, 8)}... (attempt ${retries + 1}) - status:`, hisData?.status, 'outputs:', Object.keys(hisData?.outputs ?? {}).length);
714
+ if (hisData?.status?.completed && hisData.outputs) {
715
+ console.log(`[CallWrapper] Found completed job in history with outputs - attempting to populate from history`);
716
+ break;
717
+ }
718
+ if (retries < 4) {
719
+ console.log(`[CallWrapper] History not ready yet, waiting 100ms before retry...`);
720
+ await new Promise(resolve => setTimeout(resolve, 100));
721
+ }
722
+ }
723
+ if (hisData?.status?.completed && hisData.outputs) {
724
+ // Try to extract outputs from history data
725
+ let populatedCount = 0;
726
+ for (const [nodeIdStr, nodeOutput] of Object.entries(hisData.outputs)) {
727
+ const nodeId = parseInt(nodeIdStr, 10);
728
+ const outputKey = reverseOutputMapped[nodeId];
729
+ if (outputKey && nodeOutput) {
730
+ // nodeOutput is typically { images: [...] } or similar - take the first property
731
+ const outputValue = Array.isArray(nodeOutput) ? nodeOutput[0] : Object.values(nodeOutput)[0];
732
+ if (outputValue !== undefined) {
733
+ this.output[outputKey] = outputValue;
734
+ this.onOutputFn?.(outputKey, outputValue, this.promptId);
735
+ populatedCount++;
736
+ remainingOutput--;
737
+ console.log(`[CallWrapper] Populated ${outputKey} from history`);
738
+ }
739
+ }
740
+ }
741
+ if (remainingOutput === 0) {
742
+ console.log(`[CallWrapper] Successfully populated all outputs from history for ${promptId.substring(0, 8)}...`);
743
+ this.isCompletingSuccessfully = true;
744
+ this.cleanupListeners("executedEnd - populated from history");
745
+ this.onFinishedFn?.(this.output, this.promptId);
746
+ this.resolveJob(this.output);
485
747
  return;
486
748
  }
749
+ if (populatedCount > 0) {
750
+ console.log(`[CallWrapper] Populated ${populatedCount} outputs from history (remainingOutput=${remainingOutput})`);
751
+ if (remainingOutput === 0) {
752
+ this.isCompletingSuccessfully = true;
753
+ this.cleanupListeners("executedEnd - all outputs from history");
754
+ this.onFinishedFn?.(this.output, this.promptId);
755
+ this.resolveJob(this.output);
756
+ return;
757
+ }
758
+ }
487
759
  }
488
- this.emitLog("CallWrapper.executedEnd", "execution failed due to missing outputs", {
489
- remainingOutput,
490
- totalOutput
491
- });
492
- this.onFailedFn?.(new ExecutionFailedError("Execution failed"), this.promptId);
760
+ console.log(`[CallWrapper] execution failed due to missing outputs - remainingOutput=${remainingOutput}, totalOutput=${totalOutput}`);
761
+ this.emitFailure(new ExecutionFailedError("Execution failed"), this.promptId);
762
+ this.resolvePromptLoad(false);
493
763
  this.cleanupListeners("executedEnd missing outputs");
494
- jobDoneTrigger(false);
764
+ this.resolveJob(false);
495
765
  };
496
766
  this.executionEndSuccessOffFn = this.client.on("execution_success", executedEnd);
497
767
  this.executionHandlerOffFn = this.client.on("executed", executionHandler);
498
- this.errorHandlerOffFn = this.client.on("execution_error", (ev) => this.handleError(ev, promptId, jobDoneTrigger));
768
+ console.log(`[CallWrapper] Registered listeners for ${promptId.substring(0, 8)}... - executionHandler and executedEnd`);
769
+ this.errorHandlerOffFn = this.client.on("execution_error", (ev) => this.handleError(ev, promptId));
499
770
  this.interruptionHandlerOffFn = this.client.on("execution_interrupted", (ev) => {
500
771
  if (ev.detail.prompt_id !== promptId)
501
772
  return;
502
- this.onFailedFn?.(new ExecutionInterruptedError("The execution was interrupted!", { cause: ev.detail }), ev.detail.prompt_id);
773
+ this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: ev.detail }), ev.detail.prompt_id);
774
+ this.resolvePromptLoad(false);
503
775
  this.cleanupListeners("execution interrupted");
504
- jobDoneTrigger(false);
776
+ this.resolveJob(false);
505
777
  });
506
778
  }
507
779
  reverseMapOutputKeys() {
@@ -519,16 +791,26 @@ export class CallWrapper {
519
791
  }
520
792
  this.onProgressFn?.(ev.detail, this.promptId);
521
793
  }
522
- handleError(ev, promptId, resolve) {
794
+ handleError(ev, promptId) {
523
795
  if (ev.detail.prompt_id !== promptId)
524
796
  return;
525
797
  this.emitLog("CallWrapper.handleError", ev.detail.exception_type, {
526
798
  prompt_id: ev.detail.prompt_id,
527
799
  node_id: ev.detail?.node_id
528
800
  });
529
- this.onFailedFn?.(new CustomEventError(ev.detail.exception_type, { cause: ev.detail }), ev.detail.prompt_id);
801
+ this.emitFailure(new CustomEventError(ev.detail.exception_type, { cause: ev.detail }), ev.detail.prompt_id);
802
+ if (CALL_WRAPPER_DEBUG) {
803
+ console.log("[debug] handleError after emitFailure");
804
+ }
805
+ this.resolvePromptLoad(false);
806
+ if (CALL_WRAPPER_DEBUG) {
807
+ console.log("[debug] handleError before cleanup");
808
+ }
530
809
  this.cleanupListeners("execution_error received");
531
- resolve(false);
810
+ if (CALL_WRAPPER_DEBUG) {
811
+ console.log("[debug] handleError after cleanup");
812
+ }
813
+ this.resolveJob(false);
532
814
  }
533
815
  emitLog(fnName, message, data) {
534
816
  const detail = { fnName, message, data };
@@ -543,6 +825,13 @@ export class CallWrapper {
543
825
  cleanupListeners(reason) {
544
826
  const debugPayload = { reason, promptId: this.promptId };
545
827
  this.emitLog("CallWrapper.cleanupListeners", "removing listeners", debugPayload);
828
+ this.resolvePromptLoad(false);
829
+ this.stopDisconnectRecovery();
830
+ this.onReconnectHandlerOffFn?.();
831
+ this.onReconnectHandlerOffFn = undefined;
832
+ this.onReconnectFailedHandlerOffFn?.();
833
+ this.onReconnectFailedHandlerOffFn = undefined;
834
+ this.disconnectFailureTimer = null;
546
835
  this.onDisconnectedHandlerOffFn?.();
547
836
  this.onDisconnectedHandlerOffFn = undefined;
548
837
  this.checkExecutingOffFn?.();