comfyui-mcp 0.52.26 → 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.
- package/dist/orchestrator/node-id.js +12 -2
- package/dist/orchestrator/node-id.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +301 -53
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/manager-update-error.js +189 -0
- package/dist/services/manager-update-error.js.map +1 -0
- package/dist/services/ui-bridge.js +59 -9
- package/dist/services/ui-bridge.js.map +1 -1
- package/package.json +1 -1
|
@@ -21,8 +21,18 @@
|
|
|
21
21
|
* refused to risk. So a qualified id is never converted: it stays the string the
|
|
22
22
|
* reader printed, all the way to the panel.
|
|
23
23
|
*/
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* A plain integer id, the form the wire has always carried.
|
|
26
|
+
*
|
|
27
|
+
* Exported because one argument needs this shape ON ITS OWN: panel_run's
|
|
28
|
+
* `to_node_id` (#1497). Everything else takes NODE_ID_PATTERN, which admits the
|
|
29
|
+
* qualified form too — but a run-to-node target is resolved by NUMBER all the way
|
|
30
|
+
* down (the panel's findNodeInScopes does `Number(id)`, and its reply reports
|
|
31
|
+
* `ran_to_node: Number(to_node_id)`), so admitting `"120:104"` there would trade a
|
|
32
|
+
* clear schema refusal for a panel-side "node not found" about a node that exists.
|
|
33
|
+
* The shape lives here so the two patterns cannot drift apart.
|
|
34
|
+
*/
|
|
35
|
+
export const PLAIN_NODE_ID_PATTERN = /^-?\d+$/;
|
|
26
36
|
/**
|
|
27
37
|
* A subgraph-qualified id: integer segments joined by colons (`120:104`,
|
|
28
38
|
* `120:113:78`). No depth limit — nesting is arbitrary, and a limit would fail the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-id.js","sourceRoot":"","sources":["../../src/orchestrator/node-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH
|
|
1
|
+
{"version":3,"file":"node-id.js","sourceRoot":"","sources":["../../src/orchestrator/node-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAS,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAErC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAElD,6DAA6D;AAC7D,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,iBAAiB,CAAC,CAAS;IACzC,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,CAAkB;IAChD,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAChC,CAAC;AAED;oFACoF;AACpF,MAAM,CAAC,MAAM,eAAe,GAC1B,kFAAkF,CAAC"}
|
|
@@ -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";
|
|
@@ -45,7 +46,7 @@ import { compareSemver } from "../services/self-update.js";
|
|
|
45
46
|
import { describeInstallPanelAction } from "../services/panel-recovery.js";
|
|
46
47
|
import { peekResolvedPanelBase, primePanelBase, verifiedPanelDiskVersion, } from "../services/panel-workspace.js";
|
|
47
48
|
import { conversationOfScopeAddress, isScopeAddress, shortTabId } from "../services/session-scope.js";
|
|
48
|
-
import { NODE_ID_MESSAGE, NODE_ID_PATTERN, normalizeNodeId } from "./node-id.js";
|
|
49
|
+
import { NODE_ID_MESSAGE, NODE_ID_PATTERN, normalizeNodeId, PLAIN_NODE_ID_PATTERN, } from "./node-id.js";
|
|
49
50
|
import { parseContradictoryPromotedWidgetRefusal, resolveInnerPromotedTarget, } from "./promoted-widget.js";
|
|
50
51
|
import { fastGroupsFilterPropertyNote, isFastGroupsFilterProperty, } from "./rgthree-fast-groups-property.js";
|
|
51
52
|
import { isPlainObject, isStampMismatchSaveRefusal, openLiveMatchesDestContent, patchOpenIdentity, shouldRebindOpenIdentity, workflowFromSerializeReply, } from "./open-identity-normalization.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
|
|
@@ -4973,8 +5092,18 @@ panelGapNote = "") {
|
|
|
4973
5092
|
// A panel that cannot fence a WRITE gives reads only, however good the stamp is.
|
|
4974
5093
|
const mutationsRefused = canMutate === false;
|
|
4975
5094
|
const mutationsUnknown = canMutate === undefined;
|
|
5095
|
+
// #1494 — `reads_only` would be a FALSE consolation for this one cause. The
|
|
5096
|
+
// dispatch-time agreement gate covers `graph_*` READS as well as mutations
|
|
5097
|
+
// (requiresStampTargetAgreement), so a session in that state has neither working
|
|
5098
|
+
// graph reads NOR writes — only the canvas-INDEPENDENT ops that predicate exempts
|
|
5099
|
+
// (graph_update_node, the virtual-type reads) still dispatch, which is not what a
|
|
5100
|
+
// caller reads "reads_only" as. And the whole report is that this call answered
|
|
5101
|
+
// "bound" while everything the gate covers was being refused. It is
|
|
5102
|
+
// `not_recovered`: the caller must not read success out of it.
|
|
4976
5103
|
const okBinding = mutationsRefused
|
|
4977
|
-
? "
|
|
5104
|
+
? refusalCause === "target_disagreement"
|
|
5105
|
+
? "not_recovered"
|
|
5106
|
+
: "reads_only"
|
|
4978
5107
|
: mutationsUnknown
|
|
4979
5108
|
? "unverified"
|
|
4980
5109
|
: "bound";
|
|
@@ -5000,23 +5129,37 @@ panelGapNote = "") {
|
|
|
5000
5129
|
`\n\nWHAT TO DO: call this tool again to bind an identity. If it keeps coming back ` +
|
|
5001
5130
|
`without one, have the user click into the workflow tab so the panel reports an active ` +
|
|
5002
5131
|
`canvas, then call this again.`
|
|
5003
|
-
: mutationsRefused && refusalCause === "
|
|
5004
|
-
? `\n\nBUT
|
|
5005
|
-
`
|
|
5006
|
-
`
|
|
5007
|
-
`
|
|
5008
|
-
|
|
5009
|
-
`
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5132
|
+
: mutationsRefused && refusalCause === "target_disagreement"
|
|
5133
|
+
? `\n\nBUT THIS DID NOT CLEAR THE REFUSAL, and saying otherwise is the bug this ` +
|
|
5134
|
+
`wording exists to stop (#1494). Graph commands are refused BEFORE they are ` +
|
|
5135
|
+
`dispatched, by a comparison this call does not perform: the session's stamp against ` +
|
|
5136
|
+
`the identity the ROUTED TAB last advertised in its handshake. This call compared the ` +
|
|
5137
|
+
`stamp against the LIVE canvas instead, and those are different facts — the live ` +
|
|
5138
|
+
`canvas agreeing is exactly why nothing above says "rebound". Graph READS are refused ` +
|
|
5139
|
+
`by the same gate, so this is not a reads-only session.` +
|
|
5140
|
+
`\n\nWHAT TO DO: re-open the workflow you mean with panel_open_workflow(<path>) — an ` +
|
|
5141
|
+
`open proves an identity for the tab under its current id, which is what the gate is ` +
|
|
5142
|
+
`missing; for a never-saved canvas use its routing_key from panel_list_workflows. The ` +
|
|
5143
|
+
`panel also re-advertises on its own when it notices the drift, so a command that ` +
|
|
5144
|
+
`keeps failing here may start working without you doing anything — do NOT read that as ` +
|
|
5145
|
+
`this call having repaired it.`
|
|
5146
|
+
: mutationsRefused && refusalCause === "unroutable"
|
|
5147
|
+
? `\n\nBUT this tab is NOT REACHABLE from the orchestrator right now, so graph mutations ` +
|
|
5148
|
+
`are refused — and reads are not working either, whatever this note says about them. ` +
|
|
5149
|
+
`This is NOT a panel-version problem: updating the pack and hard-refreshing cannot help, ` +
|
|
5150
|
+
`because there is nothing to refresh until the tab is connected again.` +
|
|
5151
|
+
`\n\nWHAT TO DO: check that the ComfyUI browser tab is still open and its panel is ` +
|
|
5152
|
+
`connected, then call this again.`
|
|
5153
|
+
: mutationsRefused
|
|
5154
|
+
? `\n\nBUT graph MUTATIONS are still refused for this tab: its panel build does not ` +
|
|
5155
|
+
`advertise the write-boundary workflow fence a graph edit requires, and no rebind — ` +
|
|
5156
|
+
`including calling this tool again — can add it. Reads work now (this call just read the ` +
|
|
5157
|
+
`live canvas).\n\nWHAT TO DO FOR EDITS: update the comfyui-mcp-panel pack, then have the ` +
|
|
5158
|
+
`user HARD-refresh (Ctrl+Shift+R) the ComfyUI browser tab — an open tab keeps running its ` +
|
|
5159
|
+
`cached bundle, so the capability lags the version on disk until it does. Do NOT use ` +
|
|
5160
|
+
`panel_reload for this: it has been observed to leave the tab permanently unresponsive ` +
|
|
5161
|
+
`(#803).`
|
|
5162
|
+
: "";
|
|
5020
5163
|
switch (r.status) {
|
|
5021
5164
|
case "diverged":
|
|
5022
5165
|
// #1646 — produced ONLY by a read-only probe (`adopt:false`), which the
|
|
@@ -7427,14 +7570,53 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
7427
7570
|
let verdict;
|
|
7428
7571
|
try {
|
|
7429
7572
|
const probe = await rebindWorkflowFence(ctx, { adopt: false });
|
|
7573
|
+
// #1494 — WHICH COMPARISON IS REFUSING, asked of the gate itself rather than
|
|
7574
|
+
// inferred from the probe.
|
|
7575
|
+
//
|
|
7576
|
+
// The probe reads the LIVE canvas. The dispatch-time agreement gate refuses on
|
|
7577
|
+
// the identity the ROUTED TAB last advertised. When the advertisement is the
|
|
7578
|
+
// stale side, those two disagree — the probe reports `already_current` on the
|
|
7579
|
+
// very uuid the command carried, which reads as "transient, retry" while every
|
|
7580
|
+
// retry is compared against the same unchanged advertisement and refused
|
|
7581
|
+
// identically. That is the reported contradiction: an immediate rebind
|
|
7582
|
+
// confirming the original instance, next to a refusal naming another one.
|
|
7583
|
+
//
|
|
7584
|
+
// Typed, from `tabGraphMutationCapability` (the bridge's own verdict, sharing
|
|
7585
|
+
// one predicate with `send()`), never a second reading of the refusal's prose.
|
|
7586
|
+
let gateStillRefuses = false;
|
|
7587
|
+
try {
|
|
7588
|
+
const cap = ctx.tabGraphMutationCapability?.();
|
|
7589
|
+
gateStillRefuses =
|
|
7590
|
+
cap?.known === true &&
|
|
7591
|
+
cap.canMutate === false &&
|
|
7592
|
+
cap.because === "target_disagreement";
|
|
7593
|
+
}
|
|
7594
|
+
catch {
|
|
7595
|
+
gateStillRefuses = false; // a diagnostic must never replace the outcome
|
|
7596
|
+
}
|
|
7430
7597
|
verdict =
|
|
7431
7598
|
probe.status === "already_current" && stamped && probe.uuid === stamped
|
|
7432
|
-
?
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7599
|
+
? gateStillRefuses
|
|
7600
|
+
? `\n\nCHECKED, and the two claims are BOTH true of different things: the live ` +
|
|
7601
|
+
`canvas does report the SAME workflow instance this command carried ` +
|
|
7602
|
+
`(${stamped}), and the refusal above is not about the live canvas — it is ` +
|
|
7603
|
+
`the pre-dispatch check on the identity the ROUTED TAB last advertised, ` +
|
|
7604
|
+
`which is the stale side here. So a bare retry is compared against that ` +
|
|
7605
|
+
`same unchanged value and is refused the same way; ` +
|
|
7606
|
+
`panel_set_workflow_target({mode:"current"}) reports "already current" for ` +
|
|
7607
|
+
`the same reason and does not clear it either. Nothing was applied. WHAT TO ` +
|
|
7608
|
+
`DO: re-open the workflow you mean with panel_open_workflow(<path>) — that ` +
|
|
7609
|
+
`proves an identity for the tab under its CURRENT id, which is what is ` +
|
|
7610
|
+
`missing; for a never-saved canvas use its routing_key from ` +
|
|
7611
|
+
`panel_list_workflows. The panel also re-advertises on its own once it ` +
|
|
7612
|
+
`notices the drift, so a retry may start working shortly — that is the ` +
|
|
7613
|
+
`panel repairing it, not this call.`
|
|
7614
|
+
: `\n\nCHECKED: the live canvas now reports the SAME workflow instance this ` +
|
|
7615
|
+
`command carried (${stamped}), so the mismatch was TRANSIENT: the identity ` +
|
|
7616
|
+
`flipped and settled back, which happens while a new unsaved workflow is still ` +
|
|
7617
|
+
`materialising. RETRY THIS EXACT CALL ONCE. Nothing was applied, so a retry ` +
|
|
7618
|
+
`cannot double-apply, and re-issuing the whole build would duplicate the work ` +
|
|
7619
|
+
`that already succeeded.`
|
|
7438
7620
|
: probe.status === "already_current"
|
|
7439
7621
|
? `\n\nCHECKED: the session's fence already names the live canvas ` +
|
|
7440
7622
|
`(${probe.uuid}), so it was not the stale side. Retry once — if it refuses ` +
|
|
@@ -8966,6 +9148,38 @@ const nodeId = () => z
|
|
|
8966
9148
|
z.string().regex(NODE_ID_PATTERN, NODE_ID_MESSAGE),
|
|
8967
9149
|
])
|
|
8968
9150
|
.transform(normalizeNodeId);
|
|
9151
|
+
/**
|
|
9152
|
+
* #1497 — the ONE node-id argument #845 never reached: panel_run's `to_node_id`.
|
|
9153
|
+
*
|
|
9154
|
+
* #845 made every panel tool accept back an id it had PRINTED, because the graph
|
|
9155
|
+
* readers hand out ids as strings (`summarizeNode` returns `id: node.id`, and on
|
|
9156
|
+
* the modern ComfyUI frontend that is `"11"`, not `11`). Twenty-seven node-id
|
|
9157
|
+
* arguments adopted `nodeId()`; this one kept `z.number().int()` and was missed,
|
|
9158
|
+
* so the documented round trip — take the id `panel_add_node` just returned, hand
|
|
9159
|
+
* it to panel_run — failed with a raw `expected number, received string`, exactly
|
|
9160
|
+
* as #845 described and #427 hit for panel_create_group. A canonical validator is
|
|
9161
|
+
* adopted per CALL SITE, and this call site never adopted it.
|
|
9162
|
+
*
|
|
9163
|
+
* NOT `nodeId()`, deliberately. That helper also admits the subgraph-QUALIFIED
|
|
9164
|
+
* shape (`"120:104"`, #1425) and passes it through as a string, which is right for
|
|
9165
|
+
* a write that addresses a node by key. A run-to-node target is not addressed by
|
|
9166
|
+
* key — it is an EXECUTION ROOT, resolved numerically the whole way down:
|
|
9167
|
+
* the panel's `findNodeInScopes` matches on `Number(id)`, its reply reports
|
|
9168
|
+
* `ran_to_node: Number(to_node_id)`, and two branches here (#772's stamp-race
|
|
9169
|
+
* re-issue and #468's run ticket) gate on `typeof args.to_node_id === "number"`.
|
|
9170
|
+
* Widening this argument to the qualified form would therefore turn a clear schema
|
|
9171
|
+
* refusal into a panel-side "node 120:104 was not found" about a node that plainly
|
|
9172
|
+
* exists, and silently drop the run ticket that correlates the completion. So the
|
|
9173
|
+
* spelling widens and the SHAPE does not: `11` and `"11"` both mean node 11, and
|
|
9174
|
+
* a qualified id is still refused — now by name, with a reason.
|
|
9175
|
+
*/
|
|
9176
|
+
const RUN_TO_NODE_ID_MESSAGE = 'a run-to-node target must be a plain integer node id — 42 or "42" both work. A subgraph-qualified id (e.g. "120:104") is not an execution root: pass the output node\'s own plain id, which is what panel_query_graph prints for it even when it is nested inside a subgraph';
|
|
9177
|
+
const runToNodeId = () => z
|
|
9178
|
+
.union([
|
|
9179
|
+
z.number().int(),
|
|
9180
|
+
z.string().regex(PLAIN_NODE_ID_PATTERN, RUN_TO_NODE_ID_MESSAGE),
|
|
9181
|
+
])
|
|
9182
|
+
.transform((v) => (typeof v === "number" ? v : Number.parseInt(v, 10)));
|
|
8969
9183
|
/**
|
|
8970
9184
|
* #845 — which `panel_canvas` arguments the chosen action actually consumes.
|
|
8971
9185
|
*
|
|
@@ -10238,11 +10452,9 @@ export function buildPanelToolDefs() {
|
|
|
10238
10452
|
.max(100)
|
|
10239
10453
|
.optional()
|
|
10240
10454
|
.describe("Times to queue (default 1)."),
|
|
10241
|
-
to_node_id:
|
|
10242
|
-
.number()
|
|
10243
|
-
.int()
|
|
10455
|
+
to_node_id: runToNodeId()
|
|
10244
10456
|
.optional()
|
|
10245
|
-
.describe("Output node id to render UP TO (partial execution). Omit to run the whole graph. Must be an OUTPUT node — one with is_output:true in panel_query_graph's detail rows. May be nested inside a subgraph (pass the node's own id)."),
|
|
10457
|
+
.describe("Output node id to render UP TO (partial execution). Omit to run the whole graph. Must be an OUTPUT node — one with is_output:true in panel_query_graph's detail rows. May be nested inside a subgraph (pass the node's own id). Takes the id EXACTLY as the readers print it: 11 and \"11\" both mean node 11 (#1497). A subgraph-qualified id (\"120:104\") is not an execution root and is refused."),
|
|
10246
10458
|
allow_duplicate: z
|
|
10247
10459
|
.boolean()
|
|
10248
10460
|
.optional()
|
|
@@ -10623,7 +10835,32 @@ export function buildPanelToolDefs() {
|
|
|
10623
10835
|
return appendToolResultText(res, lateAckNote);
|
|
10624
10836
|
return res;
|
|
10625
10837
|
}),
|
|
10626
|
-
def("panel_get_errors", "WHY IS THAT NODE RED / WHY DID THE RUN FAIL? The single error surface for the user's open tab: every errored node JOINED TO ITS CAUSE, which ComfyUI itself does not show — LiteGraph only paints a red outline and stores no reason, which is why users report \"red node, no error message\". Call this whenever the user mentions a red/highlighted/erroring node, a failed run, or \"required models are missing\" — instead of guessing from widget values. Each entry in `nodes[]` is the node's full detail summary plus `red_outline` and `reasons[]`, drawn from every source: `missing_model` (exact file, its models directory, the widget holding it, and a download URL when known), `missing_media` (a referenced input image/video that isn't on disk — the usual cause of a red LoadImage), `validation` (per-input errors from the last queue attempt: message, details, offending input), and `execution` (runtime failure with `exception_type`, e.g. PIL.UnidentifiedImageError). TWO THINGS THAT MAKE THIS ESSENTIAL: (1) missing model/media assets paint nodes red AS SOON AS THE WORKFLOW LOADS, long before any queue attempt — so the raw validation map is still EMPTY while the user is staring at red nodes; (2) a node that throws AT RUNTIME is never painted red at all, so it can't be spotted on the canvas — it appears here with red_outline:false. Also returns graph-level `missing_models`, `missing_media`, `missing_node_types` (or `missing_node_count`), plus the raw `node_errors` map and `last_execution_error` for reference. A ⚠️ GRAPH VALIDATION block is auto-injected at your turn start when this state changes; call this to re-check on demand (e.g. after you edit widgets/links). Read-only.", {},
|
|
10838
|
+
def("panel_get_errors", "WHY IS THAT NODE RED / WHY DID THE RUN FAIL? The single error surface for the user's open tab: every errored node JOINED TO ITS CAUSE, which ComfyUI itself does not show — LiteGraph only paints a red outline and stores no reason, which is why users report \"red node, no error message\". Call this whenever the user mentions a red/highlighted/erroring node, a failed run, or \"required models are missing\" — instead of guessing from widget values. Each entry in `nodes[]` is the node's full detail summary plus `red_outline` and `reasons[]`, drawn from every source: `missing_model` (exact file, its models directory, the widget holding it, and a download URL when known), `missing_media` (a referenced input image/video that isn't on disk — the usual cause of a red LoadImage), `validation` (per-input errors from the last queue attempt: message, details, offending input), and `execution` (runtime failure with `exception_type`, e.g. PIL.UnidentifiedImageError). TWO THINGS THAT MAKE THIS ESSENTIAL: (1) missing model/media assets paint nodes red AS SOON AS THE WORKFLOW LOADS, long before any queue attempt — so the raw validation map is still EMPTY while the user is staring at red nodes; (2) a node that throws AT RUNTIME is never painted red at all, so it can't be spotted on the canvas — it appears here with red_outline:false. Also returns graph-level `missing_models`, `missing_media`, `missing_node_types` (or `missing_node_count`), plus the raw `node_errors` map and `last_execution_error` for reference. A ⚠️ GRAPH VALIDATION block is auto-injected at your turn start when this state changes; call this to re-check on demand (e.g. after you edit widgets/links). Read-only.", {},
|
|
10839
|
+
// #1493 — graph_get_errors belongs to the #599 refresh-ack cohort above, and was the
|
|
10840
|
+
// one member of it left on the generic read default.
|
|
10841
|
+
//
|
|
10842
|
+
// Its frontend handler deliberately awaits a FORCED /object_info re-register before it
|
|
10843
|
+
// will trust a combo to clear a resolved missing-model candidate (#610), and it spends a
|
|
10844
|
+
// shared 18 000 ms elective budget across that refresh race, the /system_stats probe and the
|
|
10845
|
+
// /view media probes (GET_ERRORS_TOTAL_BUDGET_MS, panel web/js/lib/get-errors-budget.js).
|
|
10846
|
+
// That budget was sized to sit under BRIDGE_READ_DEFAULT_TIMEOUT_MS — which leaves 2 000 ms
|
|
10847
|
+
// for everything the call still has to do AFTER the waits: collectMissingAssets over the
|
|
10848
|
+
// whole graph, the per-node live combo scan, the subgraph walk, and serializing a reply big
|
|
10849
|
+
// enough that this tool ships two truncation riders for it.
|
|
10850
|
+
//
|
|
10851
|
+
// The reported sequence spends that budget exactly as designed — panel_refresh_nodes, three
|
|
10852
|
+
// promoted-widget writes on a model-loader subgraph, a save — and panel_get_errors was then
|
|
10853
|
+
// declared "backgrounded or frozen" at 20 000 ms by a tab that had just answered four
|
|
10854
|
+
// commands and did reply, late. That is the precise failure the shared budget exists to
|
|
10855
|
+
// prevent (#589): an agent left with NO error surface for a workflow whose nodes are red.
|
|
10856
|
+
//
|
|
10857
|
+
// The margin has to come from THIS side. The panel's 18 s bound must stay where it is — it
|
|
10858
|
+
// is what keeps a current panel safe in front of an OLDER orchestrator still using the 20 s
|
|
10859
|
+
// default — so the orchestrator widens its own wait to the same bounded budget its
|
|
10860
|
+
// refresh-before-validate siblings already use. This read is idempotent, so waiting longer
|
|
10861
|
+
// costs a slow reply and never a double-applied write; it stays BOUNDED (never Infinity), so
|
|
10862
|
+
// a genuinely frozen tab still fails, just at 30 s instead of 20 s.
|
|
10863
|
+
async (_args, ctx) => withTruncationHints(await ctx.call({ cmd: "graph_get_errors" }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS), [
|
|
10627
10864
|
{
|
|
10628
10865
|
flag: "truncated",
|
|
10629
10866
|
key: "truncation_hint",
|
|
@@ -12363,7 +12600,16 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
12363
12600
|
mode: z.enum(["remote", "local", "cache"]).optional().describe("DB source (default 'remote')."),
|
|
12364
12601
|
}, async (args, ctx) => {
|
|
12365
12602
|
assertPanelNotTargetedUnverifiable("panel_update_node", args.id);
|
|
12366
|
-
|
|
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
|
+
});
|
|
12367
12613
|
}),
|
|
12368
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. " +
|
|
12369
12615
|
"A drained queue (total_count: 0, is_processing: false) only means THIS queue is idle — " +
|
|
@@ -13622,16 +13868,18 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13622
13868
|
".") + argvNote + (preflightNote ? ` ${preflightNote}` : ""),
|
|
13623
13869
|
});
|
|
13624
13870
|
}),
|
|
13625
|
-
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) => {
|
|
13626
13872
|
const res = await ctx.call({ cmd: "free_vram" }, 15000);
|
|
13627
13873
|
// #1249 — ONLY a no-reply is settled server-side. An acked executor error
|
|
13628
13874
|
// (the panel's own "Failed to free VRAM: …") is a reply the bridge
|
|
13629
13875
|
// received and relayed; it already says what failed, and re-issuing from
|
|
13630
13876
|
// out here would fire a second mutation behind a verdict the caller was
|
|
13631
13877
|
// given. A tagged reply-timeout is the one case where nothing answered.
|
|
13632
|
-
if (
|
|
13633
|
-
return res;
|
|
13634
|
-
|
|
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);
|
|
13635
13883
|
}),
|
|
13636
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.", {
|
|
13637
13885
|
items: z
|