comfyui-mcp 0.50.82 → 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.
@@ -960,51 +960,13 @@ async function probeDeclineRecovery(base, windowMs, intervalMs, probeTimeoutMs,
960
960
  waited_ms: Date.now() - start,
961
961
  };
962
962
  }
963
- /**
964
- * The FIXED ComfyUI base URL to health-probe during a reboot readiness wait, or
965
- * null when we must fall back to the panel round-trip (as before #509). Captured by
966
- * the handler BEFORE it dispatches comfy_reboot and held for the whole wait.
967
- *
968
- * SECURITY (coordinator codex P1): the probe TARGET is the orchestrator's own
969
- * PROCESS-START local ComfyUI endpoint (getBootLocalComfyUIBaseUrl) — captured at
970
- * boot and IMMUTABLE. It is deliberately NOT getComfyUIBaseUrl(): that reflects the
971
- * mutable runtime config a panel `hello` can retarget (applyComfyuiUrl →
972
- * setComfyuiTarget), so a client could steer it. And it is NEVER the client-advertised
973
- * `hello.comfyui_url` (spoofable; comfyuiFetch would leak the configured ComfyUI auth
974
- * headers to an attacker-chosen origin). The tab origin is used ONLY as a gate, and the
975
- * gate reads the SERVER-OBSERVED handshake Origin (tabServerOrigin) — which the browser
976
- * sets and blocks page JS from forging — NOT the spoofable hello.comfyui_url: we
977
- * self-probe our OWN boot endpoint solely when the rebooted tab PROVABLY (by its
978
- * handshake) fronts THAT SAME instance, so a socket that merely CLAIMS the boot URL can't
979
- * ride an unrelated boot-instance cycle to a false certification (codex High). Null
980
- * (→ honest dispatched-unconfirmed) when:
981
- * - cloud OR remote mode; or
982
- * - the orchestrator didn't boot against a LOCAL loopback ComfyUI; or
983
- * - the tab isn't SERVER-TRUSTED-local (tabIsLocal — arrived on the token-less
984
- * loopback primary listener; relay/tunnel/LAN/pairing → false); or
985
- * - the tab's HANDSHAKE origin is absent, ambiguous (`localhost`), or does NOT match our
986
- * boot endpoint by scheme+host+port (concrete-family loopback-canonicalized) — i.e. it
987
- * drives a DIFFERENT instance / family, or one we can't verify; AND, because a handshake
988
- * Origin carries NO path, a boot target mounted under a basePath fails path-aware
989
- * identity and is (soundly) fail-closed to dispatched-unconfirmed.
990
- */
991
- /**
992
- * #851 — what to tell the caller about `restart_comfyui` when the panel's confirmation
993
- * card timed out.
994
- *
995
- * The old text recommended it unconditionally. It targets COMFYUI_URL, not the ComfyUI
996
- * the panel runs inside, so a reporter who followed that advice aimed a restart at a
997
- * different server than the one they had been working on — and got "No ComfyUI process
998
- * found on port 8188" while the panel was working fine on the live canvas.
999
- *
1000
- * `panelBase` comes from captureRebootHealthBase(), which is a PROOF rather than a
1001
- * comparison: null in remote/cloud mode, and whenever the tab's origin cannot be shown
1002
- * to front the local boot instance. "Unproven" is deliberately NOT folded into
1003
- * "different" — that would fire the warning on ordinary remote installs where the two
1004
- * ARE the same server, trading a wrong recommendation for a wrong alarm.
1005
- *
1006
- * Pure, so the three-way decision is testable without a live panel.
1007
- */
963
+ export function classifyTabReconnect({ serverReady, baselineCaptured, tabBack, }) {
964
+ if (tabBack === true)
965
+ return true; // a newer hello from the same tab was seen
966
+ if (!serverReady)
967
+ return "unknown"; // never watched — the server never came back
968
+ return baselineCaptured ? false : "unknown";
969
+ }
1008
970
  export function restartTimeoutFallbackAdvice({ headlessBase, panelBase, observedOrigin = null, }) {
1009
971
  if (panelBase != null && sameHttpBase(headlessBase, panelBase)) {
1010
972
  return "or use restart_comfyui to restart the server directly without a panel card.";
@@ -2615,27 +2577,53 @@ function currentWorkflowFence(ctx) {
2615
2577
  function corroborateActiveForFence(parsed) {
2616
2578
  const active = parsed.active;
2617
2579
  if (!active || typeof active !== "object") {
2618
- 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 };
2619
2583
  }
2620
2584
  // #514: the panel says so itself when its active record is not confirmed. An
2621
2585
  // explicit `false` is the panel telling us the value is untrustworthy — never
2622
2586
  // adopt through that. (Absent = an older panel that cannot say; the list
2623
2587
  // corroboration below then has to carry the whole weight.)
2624
2588
  if (parsed.active_confirmed === false) {
2625
- 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
+ };
2626
2596
  }
2627
2597
  const list = parsed.workflows;
2628
- 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)) {
2629
2604
  return {
2630
2605
  ok: false,
2631
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,
2632
2615
  };
2633
2616
  }
2634
2617
  // Only an AFFIRMATIVE per-record flag can nominate the corroborating entry;
2635
2618
  // inferring it from the active object would be circular.
2636
2619
  const flaggedActive = list.filter((w) => !!w && typeof w === "object" && w.active === true);
2637
2620
  if (flaggedActive.length === 0) {
2638
- 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
+ };
2639
2627
  }
2640
2628
  if (flaggedActive.length > 1) {
2641
2629
  // A mixed/partial snapshot. Exactly the shape that would let another canvas's
@@ -2643,6 +2631,8 @@ function corroborateActiveForFence(parsed) {
2643
2631
  return {
2644
2632
  ok: false,
2645
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,
2646
2636
  };
2647
2637
  }
2648
2638
  // Same tri-state primitive the pin path uses. `false` = they name DIFFERENT
@@ -2653,12 +2643,17 @@ function corroborateActiveForFence(parsed) {
2653
2643
  return {
2654
2644
  ok: false,
2655
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,
2656
2648
  };
2657
2649
  }
2658
2650
  if (verdict !== true) {
2659
2651
  return {
2660
2652
  ok: false,
2661
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,
2662
2657
  };
2663
2658
  }
2664
2659
  return { ok: true, active: active };
@@ -2725,6 +2720,36 @@ WHY THIS READ WAS NEEDED AT ALL: this session's panel is ${v.version}, and a ` +
2725
2720
  return "";
2726
2721
  }
2727
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];
2728
2753
  async function rebindWorkflowFence(ctx) {
2729
2754
  const tabAtStart = ctx.tabId;
2730
2755
  let before = currentWorkflowFence(ctx);
@@ -2738,29 +2763,68 @@ async function rebindWorkflowFence(ctx) {
2738
2763
  if (ctx.tabId !== tabAtStart)
2739
2764
  before = currentWorkflowFence(ctx);
2740
2765
  };
2741
- let parsed = null;
2742
- try {
2743
- const res = await ctx.call({ cmd: "workflow_list" }, 6000);
2744
- syncFenceToCurrentTab();
2745
- if (res?.isError) {
2746
- 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);
2747
2779
  }
2748
- parsed = parseToolResultJson(res);
2749
- }
2750
- catch (err) {
2751
- syncFenceToCurrentTab();
2752
- return await unreadableOrHealed(ctx, before, err instanceof Error ? err.message : String(err ?? "unknown error"));
2753
- }
2754
- if (!parsed) {
2755
- 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;
2756
2819
  }
2757
- // CORROBORATE before adopting. The uuid must come from a record the panel's own
2758
- // open-workflow list agrees is the live canvas — otherwise a stale or mixed
2759
- // reply's uuid (valid in shape, belonging to ANOTHER canvas) would overwrite
2760
- // this tab's stamp and be reported as a successful rebind.
2761
- const corroborated = corroborateActiveForFence(parsed);
2762
2820
  if (!corroborated.ok) {
2763
- 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
+ };
2764
2828
  }
2765
2829
  const active = corroborated.active;
2766
2830
  const uuid = responseWorkflowUuid(active);
@@ -3100,9 +3164,28 @@ panelGapNote = "") {
3100
3164
  `plain reload can serve the same cached bundle again; only a hard refresh replaces ` +
3101
3165
  `it. If a hard refresh does not help, the installed pack itself predates the ` +
3102
3166
  `per-workflow identity and must be UPDATED — no rebind can add it.`
3103
- : `\n\nWHAT TO DO: call this again in a moment. A stale or mixed workflow list is ` +
3104
- `usually transient it settles once the panel finishes reconciling its tabs. If ` +
3105
- `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}`);
3106
3189
  if (!r.before.known) {
3107
3190
  return {
3108
3191
  binding: "unverified",
@@ -9036,12 +9119,20 @@ export function buildPanelToolDefs() {
9036
9119
  : true
9037
9120
  : false;
9038
9121
  const graphToolsReady = tabBack && (ctx.tabCanMutateGraph ? ctx.tabCanMutateGraph() : true);
9122
+ // #654 — `ready`/`graph_tools_ready` still come from the BOOLEAN, so an
9123
+ // undetermined reconnect withholds graph tools exactly as before. Only
9124
+ // the reported observation changes.
9125
+ const tabReconnect = classifyTabReconnect({
9126
+ serverReady: recovery.ready,
9127
+ baselineCaptured: preRestartPanelIdentity != null,
9128
+ tabBack,
9129
+ });
9039
9130
  return ok({
9040
9131
  rebooting: true,
9041
9132
  ready: graphToolsReady,
9042
9133
  graph_tools_ready: graphToolsReady,
9043
9134
  server_ready: recovery.ready,
9044
- panel_tab_reconnected: tabBack,
9135
+ panel_tab_reconnected: tabReconnect,
9045
9136
  confirmed_cycle: observed, // true = we directly observed the down→up cycle
9046
9137
  recovered_ms: recovery.waited_ms,
9047
9138
  probes: recovery.attempts,
@@ -9051,8 +9142,20 @@ export function buildPanelToolDefs() {
9051
9142
  ? "ComfyUI-Manager (legacy 3.x) had no reboot endpoint; the headless managed restart " +
9052
9143
  `came back healthy in ${(recovery.waited_ms / 1000).toFixed(1)}s, but ` +
9053
9144
  (!tabBack
9054
- ? "the panel tab has NOT reconnected yet (ready:false). Wait a moment then retry, or " +
9055
- 'rebind with panel_set_workflow_target({mode:"current"}) before issuing graph tools.'
9145
+ ? tabReconnect === "unknown"
9146
+ ? "whether the panel tab reconnected could NOT be determined — no pre-restart " +
9147
+ "baseline was captured for it, so nothing was watched. WHY is not established " +
9148
+ "here, and it is one of: the tab's socket was not open at the instant the " +
9149
+ "restart was dispatched; the panel advertised no tab session id (an older " +
9150
+ "build, or its browser-tab lease was refused because a duplicate tab holds " +
9151
+ "it); or the tab did not resolve at all. The tab may well be back. Graph " +
9152
+ "tools are withheld (ready:false) because that is unproven, NOT because the " +
9153
+ 'tab is known to be gone: call panel_list_workflows or panel_set_workflow_target({mode:"current"}) ' +
9154
+ "to find out, and only refresh the browser if those also fail. If this " +
9155
+ "repeats on every restart, the panel is probably too old to advertise a tab " +
9156
+ "session id — update it."
9157
+ : "the panel tab has NOT reconnected yet (ready:false). Wait a moment then retry, or " +
9158
+ 'rebind with panel_set_workflow_target({mode:"current"}) before issuing graph tools.'
9056
9159
  : "the panel tab reconnected but cannot safely run graph mutations (ready:false), usually " +
9057
9160
  "because it is still running a stale panel bundle. Hard-refresh the ComfyUI browser tab " +
9058
9161
  "(Ctrl+Shift+R) before issuing graph tools; if that does not restore it, update the panel " +
@@ -9171,6 +9274,14 @@ export function buildPanelToolDefs() {
9171
9274
  // Old/lightweight contexts have no capability accessor, so preserve their historical
9172
9275
  // contract rather than claiming a production tab passed a check it never ran.
9173
9276
  const graphToolsReady = tabBack && (ctx.tabCanMutateGraph ? ctx.tabCanMutateGraph() : true);
9277
+ // #654 — same split as the legacy path above. The gate is unchanged:
9278
+ // `ready`/`graph_tools_ready` still key off the boolean, so an undetermined
9279
+ // reconnect still withholds graph tools. Only the reported observation changes.
9280
+ const tabReconnect = classifyTabReconnect({
9281
+ serverReady: true, // this branch only runs on a confirmed healthy cycle
9282
+ baselineCaptured: preRestartPanelIdentity != null,
9283
+ tabBack,
9284
+ });
9174
9285
  // #848: WHAT THIS RESTART DID NOT DO. "It came back healthy" answers a
9175
9286
  // different question from "did it come back with the launch arguments I just
9176
9287
  // configured?", and a user who had edited ComfyUI Desktop's saved launch args
@@ -9207,7 +9318,7 @@ export function buildPanelToolDefs() {
9207
9318
  probes: recovery.attempts,
9208
9319
  saw_down: recovery.sawDown,
9209
9320
  via: recovery.via,
9210
- panel_tab_reconnected: tabBack,
9321
+ panel_tab_reconnected: tabReconnect,
9211
9322
  note: (tabBack && !graphToolsReady
9212
9323
  ? `ComfyUI restart accepted and it is healthy again in ${(recovery.waited_ms / 1000).toFixed(1)}s` +
9213
9324
  " (observed it go down then come back); the panel tab reconnected but cannot safely run " +
@@ -9219,9 +9330,20 @@ export function buildPanelToolDefs() {
9219
9330
  (dropped ? "; connection dropped as expected while it went down" : "") +
9220
9331
  (tabBack
9221
9332
  ? "; the panel tab reconnected — graph tools are ready."
9222
- : "; ComfyUI is back but the panel tab has NOT reconnected yet (ready:false) — " +
9223
- 'wait a moment then retry, or rebind with panel_set_workflow_target({mode:"current"}) ' +
9224
- "before issuing graph tools.") +
9333
+ : tabReconnect === "unknown"
9334
+ ? "; ComfyUI is back, but whether the panel tab reconnected could NOT be determined — no " +
9335
+ "pre-restart baseline was captured for it, so nothing was watched. WHY is not " +
9336
+ "established here, and it is one of: its socket was not open at the instant the " +
9337
+ "restart was dispatched; the panel advertised no tab session id (an older build, or " +
9338
+ "a refused browser-tab lease); or the tab did not resolve at all. The tab may well " +
9339
+ "be back. Graph tools are withheld (ready:false) because that is UNPROVEN, not " +
9340
+ "because the tab is known to be gone: call panel_list_workflows or " +
9341
+ 'panel_set_workflow_target({mode:"current"}) to find out, and refresh the browser only ' +
9342
+ "if those also fail. If this repeats on every restart, the panel is probably too old " +
9343
+ "to advertise a tab session id — update it."
9344
+ : "; ComfyUI is back but the panel tab has NOT reconnected yet (ready:false) — " +
9345
+ 'wait a moment then retry, or rebind with panel_set_workflow_target({mode:"current"}) ' +
9346
+ "before issuing graph tools.") +
9225
9347
  ".") + argvNote,
9226
9348
  });
9227
9349
  }),