comfyui-mcp 0.52.27 → 0.52.28
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.
|
@@ -34,6 +34,7 @@ import { fileURLToPath } from "node:url";
|
|
|
34
34
|
import { comfyuiFetch } from "../comfyui/fetch.js";
|
|
35
35
|
import { assertPanelNotTargetedUnverifiable } from "../services/panel-pin-guard.js";
|
|
36
36
|
import { nodesInstallCommandArgs } from "../services/node-management.js";
|
|
37
|
+
import { sanitizePanelUpdateNodeResult } from "../services/manager-update-error.js";
|
|
37
38
|
import { formatQueueStatusPartialNote, getManifestPartialLeftover, } from "../services/manifest-partial.js";
|
|
38
39
|
import { searchPanelNodes } from "../services/manager-node-search.js";
|
|
39
40
|
import { isPanelAnsweredError } from "../services/panel-answered.js";
|
|
@@ -573,6 +574,11 @@ export const __panelToolsTestHooks = {
|
|
|
573
574
|
setFreeVramDirect(fn) {
|
|
574
575
|
freeVramDirectOverride = fn;
|
|
575
576
|
},
|
|
577
|
+
/** Inject a fake post-/free /system_stats read so #1866 occupancy checks can
|
|
578
|
+
* be driven without real HTTP. null restores the live readVramDevices. */
|
|
579
|
+
setReadVramDevices(fn) {
|
|
580
|
+
readVramDevicesOverride = fn;
|
|
581
|
+
},
|
|
576
582
|
/** Direct access to the #742 decline recheck loop so its hard-deadline
|
|
577
583
|
* guarantee (codex gate r2) can be unit-tested with a custom deadline. */
|
|
578
584
|
probeDeclineRecovery,
|
|
@@ -2185,17 +2191,7 @@ async function readVramDevices(base, timeoutMs) {
|
|
|
2185
2191
|
const devices = body.devices;
|
|
2186
2192
|
if (!Array.isArray(devices))
|
|
2187
2193
|
return null;
|
|
2188
|
-
return devices.map((d) =>
|
|
2189
|
-
const dev = (d ?? {});
|
|
2190
|
-
const sample = {};
|
|
2191
|
-
if (typeof dev.name === "string")
|
|
2192
|
-
sample.name = dev.name;
|
|
2193
|
-
if (typeof dev.vram_total === "number")
|
|
2194
|
-
sample.vram_total = dev.vram_total;
|
|
2195
|
-
if (typeof dev.vram_free === "number")
|
|
2196
|
-
sample.vram_free = dev.vram_free;
|
|
2197
|
-
return sample;
|
|
2198
|
-
});
|
|
2194
|
+
return devices.map((d) => sampleVramDevice(d));
|
|
2199
2195
|
}
|
|
2200
2196
|
catch {
|
|
2201
2197
|
return null; // unreachable/timed out — no numbers to report
|
|
@@ -2204,12 +2200,98 @@ async function readVramDevices(base, timeoutMs) {
|
|
|
2204
2200
|
clearTimeout(timer);
|
|
2205
2201
|
}
|
|
2206
2202
|
}
|
|
2203
|
+
function sampleVramDevice(d) {
|
|
2204
|
+
const dev = (d ?? {});
|
|
2205
|
+
const sample = {};
|
|
2206
|
+
if (typeof dev.name === "string")
|
|
2207
|
+
sample.name = dev.name;
|
|
2208
|
+
if (typeof dev.index === "number")
|
|
2209
|
+
sample.index = dev.index;
|
|
2210
|
+
if (typeof dev.vram_total === "number")
|
|
2211
|
+
sample.vram_total = dev.vram_total;
|
|
2212
|
+
if (typeof dev.vram_free === "number")
|
|
2213
|
+
sample.vram_free = dev.vram_free;
|
|
2214
|
+
if (typeof dev.torch_vram_total === "number")
|
|
2215
|
+
sample.torch_vram_total = dev.torch_vram_total;
|
|
2216
|
+
if (typeof dev.torch_vram_free === "number")
|
|
2217
|
+
sample.torch_vram_free = dev.torch_vram_free;
|
|
2218
|
+
return sample;
|
|
2219
|
+
}
|
|
2220
|
+
/** A device with at least 1 GiB of VRAM is still PINNED when less than 20% is
|
|
2221
|
+
* free after /free. CUDA context leftover on an unloaded GPU is a few hundred
|
|
2222
|
+
* MB to a couple of GiB, not 80%+ of a 21 GiB card. The reporter's Raylight
|
|
2223
|
+
* MiniMax H3 case was device 2 at ~0.8% free (~179 MiB of 21 GiB) next to
|
|
2224
|
+
* siblings at ~44% free — that occupancy is what this threshold names.
|
|
2225
|
+
*
|
|
2226
|
+
* Unknown/unreadable counters are NOT pinned: an unknown answer claims
|
|
2227
|
+
* nothing in either direction (#1473). */
|
|
2228
|
+
const PINNED_VRAM_MIN_TOTAL_BYTES = 1024 * 1024 * 1024;
|
|
2229
|
+
const PINNED_VRAM_FREE_RATIO = 0.2;
|
|
2230
|
+
function deviceStillPinned(d) {
|
|
2231
|
+
const total = d.vram_total;
|
|
2232
|
+
const free = d.vram_free;
|
|
2233
|
+
if (typeof total !== "number" || typeof free !== "number")
|
|
2234
|
+
return false;
|
|
2235
|
+
if (!Number.isFinite(total) || !Number.isFinite(free))
|
|
2236
|
+
return false;
|
|
2237
|
+
if (total < PINNED_VRAM_MIN_TOTAL_BYTES)
|
|
2238
|
+
return false;
|
|
2239
|
+
if (free < 0)
|
|
2240
|
+
return true;
|
|
2241
|
+
return free / total <= PINNED_VRAM_FREE_RATIO;
|
|
2242
|
+
}
|
|
2243
|
+
function pinnedVramDevices(devices) {
|
|
2244
|
+
return devices.filter(deviceStillPinned);
|
|
2245
|
+
}
|
|
2246
|
+
/** /free only unloads ComfyUI's model manager. Ray workers, parallel CLIP
|
|
2247
|
+
* loaders, and other custom-node allocations survive it and keep the GPU
|
|
2248
|
+
* occupied — which is why a 2xx from /free is not evidence VRAM is free. */
|
|
2249
|
+
function pinnedVramAfterFreeNote(pinned) {
|
|
2250
|
+
const named = pinned
|
|
2251
|
+
.map((d) => {
|
|
2252
|
+
if (typeof d.index === "number")
|
|
2253
|
+
return `device ${d.index}`;
|
|
2254
|
+
if (typeof d.name === "string" && d.name)
|
|
2255
|
+
return d.name;
|
|
2256
|
+
return "an unnamed device";
|
|
2257
|
+
})
|
|
2258
|
+
.join(", ");
|
|
2259
|
+
return (`ComfyUI /free accepted (unload_models + free_memory) but VRAM is STILL PINNED on ` +
|
|
2260
|
+
`${named}. /free only unloads ComfyUI's model manager — it does not terminate Ray ` +
|
|
2261
|
+
`workers or custom-node allocations (Raylight sequence-parallel, parallel CLIP loaders). ` +
|
|
2262
|
+
`This is not a successful VRAM recovery. Next: panel_restart_comfyui (last resort; ` +
|
|
2263
|
+
`refuses mid-render) after confirming with get_system_stats (action:"stats").`);
|
|
2264
|
+
}
|
|
2265
|
+
function pinnedVramAfterFreeResult(pinned, extra) {
|
|
2266
|
+
return {
|
|
2267
|
+
content: [
|
|
2268
|
+
{
|
|
2269
|
+
type: "text",
|
|
2270
|
+
text: JSON.stringify({
|
|
2271
|
+
freed: false,
|
|
2272
|
+
unload_models: true,
|
|
2273
|
+
free_memory: true,
|
|
2274
|
+
model_manager_freed: true,
|
|
2275
|
+
pinned_devices: pinned,
|
|
2276
|
+
...extra,
|
|
2277
|
+
note: pinnedVramAfterFreeNote(pinned),
|
|
2278
|
+
}, null, 2),
|
|
2279
|
+
},
|
|
2280
|
+
],
|
|
2281
|
+
isError: true,
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
/** Test injection for the post-/free /system_stats occupancy read (#1866). */
|
|
2285
|
+
let readVramDevicesOverride = null;
|
|
2286
|
+
async function readVramDevicesMaybe(base, timeoutMs) {
|
|
2287
|
+
return (readVramDevicesOverride ?? readVramDevices)(base, timeoutMs);
|
|
2288
|
+
}
|
|
2207
2289
|
/** Issue ComfyUI's /free DIRECTLY against a proven-local base and read the
|
|
2208
2290
|
* VRAM counters around it. Never throws — every failure is a value, so the
|
|
2209
2291
|
* settle can degrade to the honest outcome-unknown instead of masking the
|
|
2210
2292
|
* original timeout behind a new error. */
|
|
2211
2293
|
async function freeVramDirect(base) {
|
|
2212
|
-
const before = await
|
|
2294
|
+
const before = await readVramDevicesMaybe(base, FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
2213
2295
|
const controller = new AbortController();
|
|
2214
2296
|
const timer = setTimeout(() => controller.abort(), FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
2215
2297
|
timer.unref?.();
|
|
@@ -2234,7 +2316,7 @@ async function freeVramDirect(base) {
|
|
|
2234
2316
|
finally {
|
|
2235
2317
|
clearTimeout(timer);
|
|
2236
2318
|
}
|
|
2237
|
-
const after = await
|
|
2319
|
+
const after = await readVramDevicesMaybe(base, FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
2238
2320
|
return { ok: true, before, after };
|
|
2239
2321
|
}
|
|
2240
2322
|
/** Test injection for the direct server-side /free, so the settle can be
|
|
@@ -2277,6 +2359,21 @@ async function settleFreeVramAfterAckTimeout(ctx, timedOut) {
|
|
|
2277
2359
|
],
|
|
2278
2360
|
};
|
|
2279
2361
|
}
|
|
2362
|
+
const extra = {
|
|
2363
|
+
acknowledged: false,
|
|
2364
|
+
verified: "server-side",
|
|
2365
|
+
via: `POST ${base}/free`,
|
|
2366
|
+
...(direct.before != null ? { vram_before: direct.before } : {}),
|
|
2367
|
+
...(direct.after != null ? { vram_after: direct.after } : {}),
|
|
2368
|
+
};
|
|
2369
|
+
// #1866 — a 2xx from /free is not a free GPU. Ray/CLIP workers can keep a
|
|
2370
|
+
// device pinned. When we have occupancy numbers, they are the verdict.
|
|
2371
|
+
if (direct.after != null) {
|
|
2372
|
+
const pinned = pinnedVramDevices(direct.after);
|
|
2373
|
+
if (pinned.length > 0) {
|
|
2374
|
+
return pinnedVramAfterFreeResult(pinned, extra);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2280
2377
|
const statsNote = direct.before != null && direct.after != null
|
|
2281
2378
|
? "vram_before/vram_after are the server's own /system_stats counters around the free."
|
|
2282
2379
|
: "The /system_stats read around it did not answer, so no VRAM counters are reported — the 2xx from /free is the verification, not a measured delta.";
|
|
@@ -2284,11 +2381,7 @@ async function settleFreeVramAfterAckTimeout(ctx, timedOut) {
|
|
|
2284
2381
|
freed: true,
|
|
2285
2382
|
unload_models: true,
|
|
2286
2383
|
free_memory: true,
|
|
2287
|
-
|
|
2288
|
-
verified: "server-side",
|
|
2289
|
-
via: `POST ${base}/free`,
|
|
2290
|
-
...(direct.before != null ? { vram_before: direct.before } : {}),
|
|
2291
|
-
...(direct.after != null ? { vram_after: direct.after } : {}),
|
|
2384
|
+
...extra,
|
|
2292
2385
|
note: `The panel tab never acknowledged (frozen or backgrounded), so the free was issued ` +
|
|
2293
2386
|
`DIRECTLY to the ComfyUI server this tab provably fronts — the same /free endpoint the ` +
|
|
2294
2387
|
`panel would have called — and the server confirmed it. /free is idempotent: if the tab's ` +
|
|
@@ -2296,6 +2389,32 @@ async function settleFreeVramAfterAckTimeout(ctx, timedOut) {
|
|
|
2296
2389
|
`twice. ${statsNote}`,
|
|
2297
2390
|
});
|
|
2298
2391
|
}
|
|
2392
|
+
/**
|
|
2393
|
+
* #1866 — a panel ack of `{freed:true}` is the /free HTTP 2xx, not a measured
|
|
2394
|
+
* GPU. After the tab says it posted /free, re-read /system_stats and refuse to
|
|
2395
|
+
* claim VRAM was freed when a device is still occupied. Unreadable stats leave
|
|
2396
|
+
* the original ack UNTOUCHED: an unknown answer claims nothing extra.
|
|
2397
|
+
*/
|
|
2398
|
+
async function annotateFreeVramAck(res) {
|
|
2399
|
+
if (res.isError)
|
|
2400
|
+
return res;
|
|
2401
|
+
const parsed = parseToolResultJson(res);
|
|
2402
|
+
if (!parsed || parsed.freed !== true)
|
|
2403
|
+
return res;
|
|
2404
|
+
const base = (getComfyUIBaseUrl() || "").replace(/\/+$/, "");
|
|
2405
|
+
if (!base)
|
|
2406
|
+
return res;
|
|
2407
|
+
const devices = await readVramDevicesMaybe(base, FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
2408
|
+
if (devices == null)
|
|
2409
|
+
return res;
|
|
2410
|
+
const pinned = pinnedVramDevices(devices);
|
|
2411
|
+
if (pinned.length === 0)
|
|
2412
|
+
return res;
|
|
2413
|
+
return pinnedVramAfterFreeResult(pinned, {
|
|
2414
|
+
acknowledged: true,
|
|
2415
|
+
devices,
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2299
2418
|
// ---- panel_install_node: accepted-but-never-enqueued (#1129) ---------------
|
|
2300
2419
|
// #1143 fixed the pre-queue REFUSAL (403/404 → direct clone). This is the other
|
|
2301
2420
|
// half of the same family: legacy Manager 3.x answers the install POST with
|
|
@@ -12481,7 +12600,16 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
12481
12600
|
mode: z.enum(["remote", "local", "cache"]).optional().describe("DB source (default 'remote')."),
|
|
12482
12601
|
}, async (args, ctx) => {
|
|
12483
12602
|
assertPanelNotTargetedUnverifiable("panel_update_node", args.id);
|
|
12484
|
-
|
|
12603
|
+
const res = await ctx.call({ cmd: "graph_update_node", id: args.id, version: args.version, channel: args.channel, mode: args.mode }, 30000);
|
|
12604
|
+
// #1870 — the panel attaches /internal/logs/raw as "Manager traceback"
|
|
12605
|
+
// when do_update stores only the generic sentence. That log also holds
|
|
12606
|
+
// the previous generation error, which names the same pack, so a zip
|
|
12607
|
+
// update-git miss was reported as FalApiError. Keep Manager evidence
|
|
12608
|
+
// (res.action=update-git) and drop execution history.
|
|
12609
|
+
return sanitizePanelUpdateNodeResult(res, {
|
|
12610
|
+
id: typeof args.id === "string" ? args.id : "",
|
|
12611
|
+
version: typeof args.version === "string" ? args.version : undefined,
|
|
12612
|
+
});
|
|
12485
12613
|
}),
|
|
12486
12614
|
def("panel_node_queue_status", "Check the built-in Manager's install/update queue status (to see if a queued install finished). Read-only. " +
|
|
12487
12615
|
"A drained queue (total_count: 0, is_processing: false) only means THIS queue is idle — " +
|
|
@@ -13740,16 +13868,18 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13740
13868
|
".") + argvNote + (preflightNote ? ` ${preflightNote}` : ""),
|
|
13741
13869
|
});
|
|
13742
13870
|
}),
|
|
13743
|
-
def("panel_free_vram", "Unload all loaded models and free VRAM (ComfyUI /free). Use to unwedge a stuck/OOM ComfyUI when a cancel didn't free memory — before retrying or, last resort, restarting (panel_restart_comfyui). Does NOT restart ComfyUI; it just drops resident models and frees cached memory. If the panel tab is frozen and cannot acknowledge, the free is instead issued DIRECTLY to the ComfyUI server and verified there (same /free, idempotent) whenever the tab provably fronts the local server — otherwise the outcome is reported unknown rather than claimed.", {}, async (_args, ctx) => {
|
|
13871
|
+
def("panel_free_vram", "Unload all loaded models and free VRAM (ComfyUI /free). Use to unwedge a stuck/OOM ComfyUI when a cancel didn't free memory — before retrying or, last resort, restarting (panel_restart_comfyui). Does NOT restart ComfyUI; it just drops resident models and frees cached memory. After /free, occupancy is re-read from /system_stats: if a device remains pinned (Ray workers, parallel CLIP, custom-node allocations /free cannot terminate), the reply names those devices and does NOT claim VRAM was freed — next step is panel_restart_comfyui. If the panel tab is frozen and cannot acknowledge, the free is instead issued DIRECTLY to the ComfyUI server and verified there (same /free, idempotent) whenever the tab provably fronts the local server — otherwise the outcome is reported unknown rather than claimed.", {}, async (_args, ctx) => {
|
|
13744
13872
|
const res = await ctx.call({ cmd: "free_vram" }, 15000);
|
|
13745
13873
|
// #1249 — ONLY a no-reply is settled server-side. An acked executor error
|
|
13746
13874
|
// (the panel's own "Failed to free VRAM: …") is a reply the bridge
|
|
13747
13875
|
// received and relayed; it already says what failed, and re-issuing from
|
|
13748
13876
|
// out here would fire a second mutation behind a verdict the caller was
|
|
13749
13877
|
// given. A tagged reply-timeout is the one case where nothing answered.
|
|
13750
|
-
if (
|
|
13751
|
-
return res;
|
|
13752
|
-
|
|
13878
|
+
if (isReplyTimeoutResult(res))
|
|
13879
|
+
return settleFreeVramAfterAckTimeout(ctx, res);
|
|
13880
|
+
// #1866 — a successful /free ack is not a free GPU. Re-read occupancy
|
|
13881
|
+
// and refuse to report freed:true when a device is still pinned.
|
|
13882
|
+
return annotateFreeVramAck(res);
|
|
13753
13883
|
}),
|
|
13754
13884
|
def("panel_show_media", "Display one or more images or videos directly in the panel chat. Use this whenever the user asks to SEE or SHOW a file — a disk path you composited/downloaded/generated (absolute path on the orchestrator host) OR a ComfyUI output ref ({ filename, subfolder?, type? }). Items are rendered as media cards in the agent chat area; supply optional captions. Max 8 items per call. A path item OVER the 20 MB inline cap that is not under any directory ComfyUI serves can still be shown by passing stage:true on that item — the orchestrator COPIES it into <output>/_panel_staged (an opt-in, persistent disk write; 512 MB per-file and 2 GB total caps) and displays the copy by reference. NEVER describe an image with emoji or text placeholders — call this tool instead.", {
|
|
13755
13885
|
items: z
|