comfyui-mcp 0.50.83 → 0.50.84

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.
@@ -2577,27 +2577,53 @@ function currentWorkflowFence(ctx) {
2577
2577
  function corroborateActiveForFence(parsed) {
2578
2578
  const active = parsed.active;
2579
2579
  if (!active || typeof active !== "object") {
2580
- return { ok: false, why: "the reply carried no active-workflow record" };
2580
+ // SETTLES: mid-restore the frontend genuinely has no active workflow yet
2581
+ // that is #1184's finding in this reply's terms, and it is the window here.
2582
+ return { ok: false, why: "the reply carried no active-workflow record", settles: true };
2581
2583
  }
2582
2584
  // #514: the panel says so itself when its active record is not confirmed. An
2583
2585
  // explicit `false` is the panel telling us the value is untrustworthy — never
2584
2586
  // adopt through that. (Absent = an older panel that cannot say; the list
2585
2587
  // corroboration below then has to carry the whole weight.)
2586
2588
  if (parsed.active_confirmed === false) {
2587
- return { ok: false, why: "the panel reported its active workflow as UNCONFIRMED" };
2589
+ // SETTLES: this is the reported window itself the panel says "not yet",
2590
+ // and a moment later it says otherwise (#1292).
2591
+ return {
2592
+ ok: false,
2593
+ why: "the panel reported its active workflow as UNCONFIRMED",
2594
+ settles: true,
2595
+ };
2588
2596
  }
2589
2597
  const list = parsed.workflows;
2590
- if (!Array.isArray(list) || list.length === 0) {
2598
+ // TWO DIFFERENT FACTS, and #1292 made the difference cost real time. An ABSENT
2599
+ // `workflows` field is a property of the installed build — it will not appear
2600
+ // by waiting — while an EMPTY list is a snapshot of what is open right now,
2601
+ // which a mid-restore panel reports before its tabs come back. Folding them
2602
+ // would make every recovery on an older panel 2.9s slower for nothing.
2603
+ if (!Array.isArray(list)) {
2591
2604
  return {
2592
2605
  ok: false,
2593
2606
  why: "the reply carried no open-workflow list to corroborate the active record against",
2607
+ settles: false,
2608
+ };
2609
+ }
2610
+ if (list.length === 0) {
2611
+ return {
2612
+ ok: false,
2613
+ why: "the reply's open-workflow list was empty, so nothing corroborates the active record",
2614
+ settles: true,
2594
2615
  };
2595
2616
  }
2596
2617
  // Only an AFFIRMATIVE per-record flag can nominate the corroborating entry;
2597
2618
  // inferring it from the active object would be circular.
2598
2619
  const flaggedActive = list.filter((w) => !!w && typeof w === "object" && w.active === true);
2599
2620
  if (flaggedActive.length === 0) {
2600
- return { ok: false, why: "no entry in the open-workflow list is marked active" };
2621
+ // SETTLES: a snapshot taken before the panel re-flagged its active tab.
2622
+ return {
2623
+ ok: false,
2624
+ why: "no entry in the open-workflow list is marked active",
2625
+ settles: true,
2626
+ };
2601
2627
  }
2602
2628
  if (flaggedActive.length > 1) {
2603
2629
  // A mixed/partial snapshot. Exactly the shape that would let another canvas's
@@ -2605,6 +2631,8 @@ function corroborateActiveForFence(parsed) {
2605
2631
  return {
2606
2632
  ok: false,
2607
2633
  why: `${flaggedActive.length} entries in the open-workflow list are marked active (a mixed reply)`,
2634
+ // SETTLES: "mixed" IS the half-reconciled state, by definition.
2635
+ settles: true,
2608
2636
  };
2609
2637
  }
2610
2638
  // Same tri-state primitive the pin path uses. `false` = they name DIFFERENT
@@ -2615,12 +2643,17 @@ function corroborateActiveForFence(parsed) {
2615
2643
  return {
2616
2644
  ok: false,
2617
2645
  why: "the active record and the entry marked active in the open-workflow list name DIFFERENT workflows (a stale or mixed reply)",
2646
+ // SETTLES: a stale half of the snapshot catches up.
2647
+ settles: true,
2618
2648
  };
2619
2649
  }
2620
2650
  if (verdict !== true) {
2621
2651
  return {
2622
2652
  ok: false,
2623
2653
  why: "the active record and the open-workflow list share no comparable identity field, so nothing corroborates it",
2654
+ // DOES NOT SETTLE: a field the records do not carry will not appear by
2655
+ // waiting — that is the reply's SHAPE, not its timing.
2656
+ settles: false,
2624
2657
  };
2625
2658
  }
2626
2659
  return { ok: true, active: active };
@@ -2687,6 +2720,36 @@ WHY THIS READ WAS NEEDED AT ALL: this session's panel is ${v.version}, and a ` +
2687
2720
  return "";
2688
2721
  }
2689
2722
  }
2723
+ /**
2724
+ * #1292 — THE RECONCILIATION WINDOW IS OURS TO ABSORB, NOT THE CALLER'S TO WAIT OUT.
2725
+ *
2726
+ * Right after `panel_restart_comfyui` and the tab's reconnect, the panel is
2727
+ * briefly mid-reconciliation: it answers `workflow_list`, but its active record
2728
+ * is marked UNCONFIRMED (or the list is momentarily mixed). Corroboration
2729
+ * correctly refuses — and the caller got a hard failure for a state that fixes
2730
+ * itself, with the remedy "call this again in a moment". The reporter did, and
2731
+ * the identical call returned `already_current`.
2732
+ *
2733
+ * A recovery operation that tells you to retry it is one that has not finished
2734
+ * recovering. So this path rechecks with a FRESH read, three times over ~2.9s.
2735
+ *
2736
+ * WHY THESE AND NOT A LONGER BUDGET: this runs only when the rebind has already
2737
+ * failed, so the cost is paid on a path that was going to fail anyway — but it is
2738
+ * still latency added to a tool call, and the window this covers is a local
2739
+ * process reconnecting. The reporter's five seconds is an upper bound with a
2740
+ * human in it, not a measurement of the settle.
2741
+ *
2742
+ * WHY ONLY `uncorroborated`: the sibling failure `no_uuid` means the installed
2743
+ * panel exposes no workflow identity at all. Rechecking a build that will never
2744
+ * answer differently just makes the same error slower — the distinction the
2745
+ * status already draws, now with teeth.
2746
+ *
2747
+ * The corroboration rule itself does NOT move. It still fails closed on every
2748
+ * attempt; a recheck buys a fresh snapshot to judge, never a lower bar. What
2749
+ * would be unsafe is arbitrating a mixed reply, and that is exactly what is not
2750
+ * happening here.
2751
+ */
2752
+ const FENCE_CORROBORATION_RECHECK_STEPS_MS = [400, 900, 1600];
2690
2753
  async function rebindWorkflowFence(ctx) {
2691
2754
  const tabAtStart = ctx.tabId;
2692
2755
  let before = currentWorkflowFence(ctx);
@@ -2700,29 +2763,68 @@ async function rebindWorkflowFence(ctx) {
2700
2763
  if (ctx.tabId !== tabAtStart)
2701
2764
  before = currentWorkflowFence(ctx);
2702
2765
  };
2703
- let parsed = null;
2704
- try {
2705
- const res = await ctx.call({ cmd: "workflow_list" }, 6000);
2706
- syncFenceToCurrentTab();
2707
- if (res?.isError) {
2708
- return await unreadableOrHealed(ctx, before, toolResultText(res));
2766
+ // One read + corroboration. Returns null when the READ failed in a way that is
2767
+ // its own status (`unreadable`/`healed_by_panel`) — those are not the transient
2768
+ // window this rechecks, and the settle inside unreadableOrHealed already covers
2769
+ // the case where the refusal itself repaired the fence.
2770
+ const readAndCorroborate = async () => {
2771
+ let parsed = null;
2772
+ try {
2773
+ const res = await ctx.call({ cmd: "workflow_list" }, 6000);
2774
+ syncFenceToCurrentTab();
2775
+ if (res?.isError) {
2776
+ return { done: await unreadableOrHealed(ctx, before, toolResultText(res)) };
2777
+ }
2778
+ parsed = parseToolResultJson(res);
2709
2779
  }
2710
- parsed = parseToolResultJson(res);
2711
- }
2712
- catch (err) {
2713
- syncFenceToCurrentTab();
2714
- return await unreadableOrHealed(ctx, before, err instanceof Error ? err.message : String(err ?? "unknown error"));
2715
- }
2716
- if (!parsed) {
2717
- return await unreadableOrHealed(ctx, before, "the panel's workflow_list reply was not readable as JSON");
2780
+ catch (err) {
2781
+ syncFenceToCurrentTab();
2782
+ return {
2783
+ done: await unreadableOrHealed(ctx, before, err instanceof Error ? err.message : String(err ?? "unknown error")),
2784
+ };
2785
+ }
2786
+ if (!parsed) {
2787
+ return {
2788
+ done: await unreadableOrHealed(ctx, before, "the panel's workflow_list reply was not readable as JSON"),
2789
+ };
2790
+ }
2791
+ // CORROBORATE before adopting. The uuid must come from a record the panel's own
2792
+ // open-workflow list agrees is the live canvas — otherwise a stale or mixed
2793
+ // reply's uuid (valid in shape, belonging to ANOTHER canvas) would overwrite
2794
+ // this tab's stamp and be reported as a successful rebind.
2795
+ return { corroborated: corroborateActiveForFence(parsed) };
2796
+ };
2797
+ const first = await readAndCorroborate();
2798
+ if ("done" in first)
2799
+ return first.done;
2800
+ let corroborated = first.corroborated;
2801
+ let attempts = 1;
2802
+ let waitedMs = 0;
2803
+ // #1292 — recheck the RECONCILIATION WINDOW with a fresh read, never a lower bar.
2804
+ // Only a failure that CAN settle is worth re-reading (codex review, P2): a reply
2805
+ // whose SHAPE is wrong — an older build that sends no `workflows` array, records
2806
+ // sharing no comparable identity field — will say exactly the same thing 2.9s
2807
+ // later, so retrying it just makes the identical error slower. That is the same
2808
+ // reasoning that keeps `no_uuid` out of this loop, applied one level down.
2809
+ for (const waitMs of FENCE_CORROBORATION_RECHECK_STEPS_MS) {
2810
+ if (corroborated.ok || !corroborated.settles)
2811
+ break;
2812
+ await sleep(waitMs);
2813
+ waitedMs += waitMs;
2814
+ attempts++;
2815
+ const next = await readAndCorroborate();
2816
+ if ("done" in next)
2817
+ return next.done;
2818
+ corroborated = next.corroborated;
2718
2819
  }
2719
- // CORROBORATE before adopting. The uuid must come from a record the panel's own
2720
- // open-workflow list agrees is the live canvas — otherwise a stale or mixed
2721
- // reply's uuid (valid in shape, belonging to ANOTHER canvas) would overwrite
2722
- // this tab's stamp and be reported as a successful rebind.
2723
- const corroborated = corroborateActiveForFence(parsed);
2724
2820
  if (!corroborated.ok) {
2725
- return { status: "no_identity", before, kind: "uncorroborated", why: corroborated.why };
2821
+ return {
2822
+ status: "no_identity",
2823
+ before,
2824
+ kind: "uncorroborated",
2825
+ why: corroborated.why,
2826
+ rechecks: { attempts, waitedMs, settles: corroborated.settles },
2827
+ };
2726
2828
  }
2727
2829
  const active = corroborated.active;
2728
2830
  const uuid = responseWorkflowUuid(active);
@@ -3062,9 +3164,28 @@ panelGapNote = "") {
3062
3164
  `plain reload can serve the same cached bundle again; only a hard refresh replaces ` +
3063
3165
  `it. If a hard refresh does not help, the installed pack itself predates the ` +
3064
3166
  `per-workflow identity and must be UPDATED — no rebind can add it.`
3065
- : `\n\nWHAT TO DO: call this again in a moment. A stale or mixed workflow list is ` +
3066
- `usually transient it settles once the panel finishes reconciling its tabs. If ` +
3067
- `it persists across a few attempts: ${RELOAD_TAB_REMEDY}`;
3167
+ : // #1292 DO NOT PRESCRIBE WHAT WE JUST DID. This path now rechecks
3168
+ // with fresh reads before failing, so "call this again in a moment"
3169
+ // would be advice the tool already took on the caller's behalf — and
3170
+ // taking it a fourth time is the least likely thing left to work.
3171
+ // When the rechecks ran, say so and move on to the remedy that has
3172
+ // not been tried.
3173
+ (r.rechecks && !r.rechecks.settles
3174
+ ? // Waiting cannot fix a reply whose SHAPE is wrong, so do not
3175
+ // prescribe waiting. This said "call this again in a moment" to
3176
+ // a caller whose panel will answer identically forever.
3177
+ `\n\nWHY RETRYING WILL NOT HELP: this is the shape of the panel's reply, not a ` +
3178
+ `moment in its reconciliation — a later read answers the same way. ` +
3179
+ `${RELOAD_TAB_REMEDY} If it survives a hard refresh, the installed pack is too ` +
3180
+ `old to corroborate its own active workflow and must be UPDATED.`
3181
+ : r.rechecks && r.rechecks.attempts > 1
3182
+ ? `\n\nALREADY TRIED: the panel was re-read ${r.rechecks.attempts} times over ` +
3183
+ `~${(r.rechecks.waitedMs / 1000).toFixed(1)}s and never corroborated. That window ` +
3184
+ `covers the usual post-restart reconciliation, so this is probably NOT the ` +
3185
+ `transient case.\n\nWHAT TO DO: ${RELOAD_TAB_REMEDY}`
3186
+ : `\n\nWHAT TO DO: call this again in a moment. A stale or mixed workflow list is ` +
3187
+ `usually transient — it settles once the panel finishes reconciling its tabs. ` +
3188
+ `If it persists across a few attempts: ${RELOAD_TAB_REMEDY}`);
3068
3189
  if (!r.before.known) {
3069
3190
  return {
3070
3191
  binding: "unverified",