comfyui-mcp 0.52.2 → 0.52.3
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/panel-tools.js +253 -31
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/manifest.js +27 -60
- package/dist/services/manifest.js.map +1 -1
- package/dist/services/node-authoring.js +25 -12
- package/dist/services/node-authoring.js.map +1 -1
- package/dist/services/process-control.js +52 -1
- package/dist/services/process-control.js.map +1 -1
- package/dist/services/queue-monitor.js +51 -2
- package/dist/services/queue-monitor.js.map +1 -1
- package/dist/services/trainer-bootstrap.js +51 -5
- package/dist/services/trainer-bootstrap.js.map +1 -1
- package/dist/services/workspace-env.js +40 -0
- package/dist/services/workspace-env.js.map +1 -1
- package/dist/tools/node-pack.js +20 -4
- package/dist/tools/node-pack.js.map +1 -1
- package/dist/tools/vocabulary.js +350 -0
- package/dist/tools/vocabulary.js.map +1 -1
- package/dist/utils/origin.js +12 -2
- package/dist/utils/origin.js.map +1 -1
- package/package.json +1 -1
|
@@ -543,6 +543,11 @@ export const __panelToolsTestHooks = {
|
|
|
543
543
|
setDeclineProbeTiming(timing) {
|
|
544
544
|
declineProbeTimingOverride = timing;
|
|
545
545
|
},
|
|
546
|
+
/** Inject a fake #1249 server-side /free so the frozen-tab settle can be
|
|
547
|
+
* driven without real HTTP. null restores the live freeVramDirect. */
|
|
548
|
+
setFreeVramDirect(fn) {
|
|
549
|
+
freeVramDirectOverride = fn;
|
|
550
|
+
},
|
|
546
551
|
/** Direct access to the #742 decline recheck loop so its hard-deadline
|
|
547
552
|
* guarantee (codex gate r2) can be unit-tested with a custom deadline. */
|
|
548
553
|
probeDeclineRecovery,
|
|
@@ -1904,6 +1909,167 @@ async function settleExitSubgraphAfterAckTimeout(ctx, timedOut) {
|
|
|
1904
1909
|
}
|
|
1905
1910
|
return timedOut;
|
|
1906
1911
|
}
|
|
1912
|
+
// ---- panel_free_vram: verifiable when the canvas tab is frozen (#1249) -----
|
|
1913
|
+
// `free_vram` is a purely SERVER-SIDE operation — the panel's handler is a plain
|
|
1914
|
+
// `POST /free` against the ComfyUI the tab fronts — yet the tool treated the
|
|
1915
|
+
// frozen tab's ACK as the only source of truth: a reply timeout left the caller
|
|
1916
|
+
// with "MUTATES … may have been applied" and no way to verify recovery. The
|
|
1917
|
+
// settle below takes the path the tab was only proxying: when the tab PROVABLY
|
|
1918
|
+
// fronts the orchestrator's local boot instance (the captureRebootHealthBase
|
|
1919
|
+
// gate — loopback, server-trusted, handshake-origin-matched), issue /free
|
|
1920
|
+
// DIRECTLY and read /system_stats around it.
|
|
1921
|
+
//
|
|
1922
|
+
// Why re-issuing is safe HERE and nowhere else on the timeout path: /free is
|
|
1923
|
+
// IDEMPOTENT. Unloading already-unloaded models and freeing an already-empty
|
|
1924
|
+
// cache is a no-op, so the copy the frozen tab may still execute when it wakes
|
|
1925
|
+
// cannot double-apply — the exact hazard the bridge's "do not blind-retry"
|
|
1926
|
+
// disclosure guards against for every other mutation does not exist for this
|
|
1927
|
+
// one command. This is a per-command exception, argued per command; it is NOT a
|
|
1928
|
+
// precedent for settling other mutations this way.
|
|
1929
|
+
/** Per-request bound for the direct /free + /system_stats round-trips, so a
|
|
1930
|
+
* wedged server degrades to the honest outcome-unknown instead of hanging the
|
|
1931
|
+
* tool call. ComfyUI applies /free synchronously before answering, so a large
|
|
1932
|
+
* unload can take seconds — 10s matches the bound probeComfyEndpoint callers
|
|
1933
|
+
* already pay on this same server. */
|
|
1934
|
+
const FREE_VRAM_DIRECT_TIMEOUT_MS = 10_000;
|
|
1935
|
+
/** GET `${base}/system_stats` and return its device VRAM counters, or null when
|
|
1936
|
+
* the read cannot answer (unreachable, non-2xx, non-ComfyUI body). Never
|
|
1937
|
+
* throws: an unreadable stat is "no numbers", never evidence in either
|
|
1938
|
+
* direction — the POST's own status is what certifies the free. */
|
|
1939
|
+
async function readVramDevices(base, timeoutMs) {
|
|
1940
|
+
const controller = new AbortController();
|
|
1941
|
+
const timer = setTimeout(() => controller.abort(), Math.max(1, timeoutMs));
|
|
1942
|
+
timer.unref?.();
|
|
1943
|
+
try {
|
|
1944
|
+
const res = await comfyuiFetch(`${base}/system_stats`, {
|
|
1945
|
+
signal: controller.signal,
|
|
1946
|
+
redirect: "manual",
|
|
1947
|
+
});
|
|
1948
|
+
if (res.status < 200 || res.status >= 300)
|
|
1949
|
+
return null;
|
|
1950
|
+
let body;
|
|
1951
|
+
try {
|
|
1952
|
+
body = await res.json();
|
|
1953
|
+
}
|
|
1954
|
+
catch {
|
|
1955
|
+
return null; // 2xx but not JSON — up, but not a /system_stats we trust
|
|
1956
|
+
}
|
|
1957
|
+
if (!looksLikeSystemStats(body))
|
|
1958
|
+
return null;
|
|
1959
|
+
const devices = body.devices;
|
|
1960
|
+
if (!Array.isArray(devices))
|
|
1961
|
+
return null;
|
|
1962
|
+
return devices.map((d) => {
|
|
1963
|
+
const dev = (d ?? {});
|
|
1964
|
+
const sample = {};
|
|
1965
|
+
if (typeof dev.name === "string")
|
|
1966
|
+
sample.name = dev.name;
|
|
1967
|
+
if (typeof dev.vram_total === "number")
|
|
1968
|
+
sample.vram_total = dev.vram_total;
|
|
1969
|
+
if (typeof dev.vram_free === "number")
|
|
1970
|
+
sample.vram_free = dev.vram_free;
|
|
1971
|
+
return sample;
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
catch {
|
|
1975
|
+
return null; // unreachable/timed out — no numbers to report
|
|
1976
|
+
}
|
|
1977
|
+
finally {
|
|
1978
|
+
clearTimeout(timer);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
/** Issue ComfyUI's /free DIRECTLY against a proven-local base and read the
|
|
1982
|
+
* VRAM counters around it. Never throws — every failure is a value, so the
|
|
1983
|
+
* settle can degrade to the honest outcome-unknown instead of masking the
|
|
1984
|
+
* original timeout behind a new error. */
|
|
1985
|
+
async function freeVramDirect(base) {
|
|
1986
|
+
const before = await readVramDevices(base, FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
1987
|
+
const controller = new AbortController();
|
|
1988
|
+
const timer = setTimeout(() => controller.abort(), FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
1989
|
+
timer.unref?.();
|
|
1990
|
+
try {
|
|
1991
|
+
const res = await comfyuiFetch(`${base}/free`, {
|
|
1992
|
+
method: "POST",
|
|
1993
|
+
headers: { "Content-Type": "application/json" },
|
|
1994
|
+
body: JSON.stringify({ unload_models: true, free_memory: true }),
|
|
1995
|
+
signal: controller.signal,
|
|
1996
|
+
redirect: "manual",
|
|
1997
|
+
});
|
|
1998
|
+
if (res.status < 200 || res.status >= 300) {
|
|
1999
|
+
return { ok: false, reason: `POST ${base}/free answered HTTP ${res.status}` };
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
catch (err) {
|
|
2003
|
+
const msg = controller.signal.aborted
|
|
2004
|
+
? `POST ${base}/free did not answer within ${FREE_VRAM_DIRECT_TIMEOUT_MS} ms`
|
|
2005
|
+
: `POST ${base}/free failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
2006
|
+
return { ok: false, reason: msg };
|
|
2007
|
+
}
|
|
2008
|
+
finally {
|
|
2009
|
+
clearTimeout(timer);
|
|
2010
|
+
}
|
|
2011
|
+
const after = await readVramDevices(base, FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
2012
|
+
return { ok: true, before, after };
|
|
2013
|
+
}
|
|
2014
|
+
/** Test injection for the direct server-side /free, so the settle can be
|
|
2015
|
+
* driven without real HTTP. null restores the live path. */
|
|
2016
|
+
let freeVramDirectOverride = null;
|
|
2017
|
+
/**
|
|
2018
|
+
* After an ack timeout on `free_vram`, settle the outcome against the one
|
|
2019
|
+
* channel a frozen tab cannot block: the ComfyUI server itself.
|
|
2020
|
+
*
|
|
2021
|
+
* Returns the timeout UNTOUCHED in every case where nothing was verified —
|
|
2022
|
+
* no provable local server (remote/cloud tab, ambiguous origin, untrusted
|
|
2023
|
+
* socket), or a direct /free that itself failed. #1473's rule: an unknown
|
|
2024
|
+
* answer claims nothing in either direction, and the bridge's original
|
|
2025
|
+
* outcome-unknown disclosure is already the honest verdict there.
|
|
2026
|
+
*/
|
|
2027
|
+
async function settleFreeVramAfterAckTimeout(ctx, timedOut) {
|
|
2028
|
+
// The same gate the restart certification uses: null unless the tab PROVABLY
|
|
2029
|
+
// fronts THIS orchestrator's local boot instance (loopback + server-trusted +
|
|
2030
|
+
// handshake-origin match). Without that proof a direct /free could aim at a
|
|
2031
|
+
// DIFFERENT server than the one the tab was asked to free — reporting that as
|
|
2032
|
+
// this command's success would be the wrong-target success the gate exists to
|
|
2033
|
+
// prevent, which is worse than the honest unknown being fixed.
|
|
2034
|
+
const base = captureRebootHealthBase(ctx);
|
|
2035
|
+
if (!base)
|
|
2036
|
+
return timedOut;
|
|
2037
|
+
const direct = await (freeVramDirectOverride ?? freeVramDirect)(base);
|
|
2038
|
+
if (!direct.ok) {
|
|
2039
|
+
// The tab never answered AND the server-side path failed. The outcome stays
|
|
2040
|
+
// unknown — say so, and name what was tried, without claiming either way.
|
|
2041
|
+
const text = timedOut.content?.find((c) => c.type === "text")?.text ?? "";
|
|
2042
|
+
return {
|
|
2043
|
+
...timedOut,
|
|
2044
|
+
content: [
|
|
2045
|
+
{
|
|
2046
|
+
type: "text",
|
|
2047
|
+
text: `${text}\n\nThe server-side fallback also could not reach ComfyUI's /free ` +
|
|
2048
|
+
`(${direct.reason ?? "no detail"}), so whether VRAM was freed remains UNVERIFIED — ` +
|
|
2049
|
+
`check with get_system_stats (action:"health") once the server answers.`,
|
|
2050
|
+
},
|
|
2051
|
+
],
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
const statsNote = direct.before != null && direct.after != null
|
|
2055
|
+
? "vram_before/vram_after are the server's own /system_stats counters around the free."
|
|
2056
|
+
: "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.";
|
|
2057
|
+
return ok({
|
|
2058
|
+
freed: true,
|
|
2059
|
+
unload_models: true,
|
|
2060
|
+
free_memory: true,
|
|
2061
|
+
acknowledged: false,
|
|
2062
|
+
verified: "server-side",
|
|
2063
|
+
via: `POST ${base}/free`,
|
|
2064
|
+
...(direct.before != null ? { vram_before: direct.before } : {}),
|
|
2065
|
+
...(direct.after != null ? { vram_after: direct.after } : {}),
|
|
2066
|
+
note: `The panel tab never acknowledged (frozen or backgrounded), so the free was issued ` +
|
|
2067
|
+
`DIRECTLY to the ComfyUI server this tab provably fronts — the same /free endpoint the ` +
|
|
2068
|
+
`panel would have called — and the server confirmed it. /free is idempotent: if the tab's ` +
|
|
2069
|
+
`queued copy still executes when the tab wakes, it is a no-op, so nothing was applied ` +
|
|
2070
|
+
`twice. ${statsNote}`,
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
1907
2073
|
// ---- panel_install_node: accepted-but-never-enqueued (#1129) ---------------
|
|
1908
2074
|
// #1143 fixed the pre-queue REFUSAL (403/404 → direct clone). This is the other
|
|
1909
2075
|
// half of the same family: legacy Manager 3.x answers the install POST with
|
|
@@ -3860,7 +4026,7 @@ NOTE: an API-format load CAN re-mint the canvas workflow instance. If your next
|
|
|
3860
4026
|
`Clear it with panel_set_workflow_target({mode:"current"}), which re-derives the fence ` +
|
|
3861
4027
|
`from the live canvas, then retry. If the next command is not refused, nothing needs doing.`);
|
|
3862
4028
|
}
|
|
3863
|
-
async function rebindWorkflowFence(ctx) {
|
|
4029
|
+
async function rebindWorkflowFence(ctx, opts) {
|
|
3864
4030
|
const tabAtStart = ctx.tabId;
|
|
3865
4031
|
let before = currentWorkflowFence(ctx);
|
|
3866
4032
|
// `before` describes the tab we are ABOUT to compare against — but ctx.call can
|
|
@@ -3952,6 +4118,13 @@ async function rebindWorkflowFence(ctx) {
|
|
|
3952
4118
|
// the stamp already matched without ever having read it.
|
|
3953
4119
|
if (before.known && before.uuid === uuid)
|
|
3954
4120
|
return { status: "already_current", uuid, before };
|
|
4121
|
+
// #1646 — a READ-ONLY probe never moves the fence: the live canvas naming a
|
|
4122
|
+
// DIFFERENT workflow is reported, not adopted. Only a deliberate rebind
|
|
4123
|
+
// (panel_set_workflow_target, open/new) may replace the fence — a mismatch
|
|
4124
|
+
// diagnosis that re-pointed the session on its own authority routed the
|
|
4125
|
+
// caller's NEXT edits onto the very canvas the refusal named as the wrong one.
|
|
4126
|
+
if (opts?.adopt === false)
|
|
4127
|
+
return { status: "diverged", uuid, before };
|
|
3955
4128
|
// refreshWorkflowUuid routes through the orchestrator's validator, which
|
|
3956
4129
|
// re-checks reachability and the uuid's shape/origin binding. A `false` here is
|
|
3957
4130
|
// a REFUSAL, not a no-op, so it gets its own status rather than being reported
|
|
@@ -4095,6 +4268,21 @@ panelGapNote = "") {
|
|
|
4095
4268
|
`(#803).`
|
|
4096
4269
|
: "";
|
|
4097
4270
|
switch (r.status) {
|
|
4271
|
+
case "diverged":
|
|
4272
|
+
// #1646 — produced ONLY by a read-only probe (`adopt:false`), which the
|
|
4273
|
+
// mismatch diagnosis uses; the deliberate rebinds this renderer serves
|
|
4274
|
+
// never pass it. Handled anyway, because an unhandled union member would
|
|
4275
|
+
// silently render as `undefined` — say exactly what happened if a future
|
|
4276
|
+
// caller ever routes one here.
|
|
4277
|
+
return {
|
|
4278
|
+
binding: "not_recovered",
|
|
4279
|
+
note: ` The live canvas is a DIFFERENT workflow instance (${r.uuid}) than this session's ` +
|
|
4280
|
+
`fence, and it was deliberately NOT adopted — a diagnosis must never re-point ` +
|
|
4281
|
+
`mutation routing on its own.` +
|
|
4282
|
+
(r.before.known && r.before.uuid ? ` The fence still names ${r.before.uuid}.` : "") +
|
|
4283
|
+
`\n\nWHAT TO DO: re-open the workflow you mean with panel_open_workflow, or re-target ` +
|
|
4284
|
+
`the live canvas deliberately by calling this tool with mode:"current".`,
|
|
4285
|
+
};
|
|
4098
4286
|
case "refreshed":
|
|
4099
4287
|
return {
|
|
4100
4288
|
binding: okBinding,
|
|
@@ -6152,9 +6340,16 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
6152
6340
|
// because nothing in the refusal distinguishes "the canvas really is a different
|
|
6153
6341
|
// workflow" from "the identity flipped for a moment while you were building".
|
|
6154
6342
|
//
|
|
6155
|
-
// The fence is NOT weakened
|
|
6156
|
-
//
|
|
6157
|
-
//
|
|
6343
|
+
// The fence is NOT weakened, nothing is auto-applied, and — #1646 — the
|
|
6344
|
+
// probe is READ-ONLY. The first version of this check re-derived the fence
|
|
6345
|
+
// onto the live canvas when the two genuinely differed ("AUTO-REBIND"), so
|
|
6346
|
+
// every later mutation in the caller's sequence was silently re-pointed at
|
|
6347
|
+
// the very canvas the refusal had just named as the wrong one — the exact
|
|
6348
|
+
// corruption the fence exists to prevent, delivered as recovery. Now the
|
|
6349
|
+
// check says which of the two states it found and the fence moves ONLY on
|
|
6350
|
+
// an explicit rebind: panel_set_workflow_target({mode:"current"}) or a
|
|
6351
|
+
// successful open. Until then every write stays refused against the target
|
|
6352
|
+
// the caller actually named. One informed retry replaces fourteen blind ones.
|
|
6158
6353
|
//
|
|
6159
6354
|
// Safe to recommend a retry because a fence refusal is checked BEFORE the handler
|
|
6160
6355
|
// runs — "Nothing was applied" is structural here, not an echoed claim.
|
|
@@ -6170,33 +6365,47 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
6170
6365
|
const stamped = /issued for workflow instance ([0-9a-f-]{36})/i.exec(err instanceof Error ? err.message : String(err))?.[1] ?? null;
|
|
6171
6366
|
let verdict;
|
|
6172
6367
|
try {
|
|
6173
|
-
const
|
|
6368
|
+
const probe = await rebindWorkflowFence(ctx, { adopt: false });
|
|
6174
6369
|
verdict =
|
|
6175
|
-
|
|
6176
|
-
? `\n\
|
|
6177
|
-
`
|
|
6178
|
-
`
|
|
6179
|
-
`
|
|
6180
|
-
`
|
|
6181
|
-
`
|
|
6182
|
-
:
|
|
6183
|
-
? `\n\
|
|
6184
|
-
`(${
|
|
6370
|
+
probe.status === "already_current" && stamped && probe.uuid === stamped
|
|
6371
|
+
? `\n\nCHECKED: the live canvas now reports the SAME workflow instance this ` +
|
|
6372
|
+
`command carried (${stamped}), so the mismatch was TRANSIENT: the identity ` +
|
|
6373
|
+
`flipped and settled back, which happens while a new unsaved workflow is still ` +
|
|
6374
|
+
`materialising. RETRY THIS EXACT CALL ONCE. Nothing was applied, so a retry ` +
|
|
6375
|
+
`cannot double-apply, and re-issuing the whole build would duplicate the work ` +
|
|
6376
|
+
`that already succeeded.`
|
|
6377
|
+
: probe.status === "already_current"
|
|
6378
|
+
? `\n\nCHECKED: the session's fence already names the live canvas ` +
|
|
6379
|
+
`(${probe.uuid}), so it was not the stale side. Retry once — if it refuses ` +
|
|
6185
6380
|
`again with the same pair, the two identities are genuinely disagreeing and ` +
|
|
6186
6381
|
`panel_open_workflow is the way to settle which one you mean.`
|
|
6187
|
-
:
|
|
6188
|
-
? `\n\
|
|
6189
|
-
`(
|
|
6190
|
-
`
|
|
6191
|
-
|
|
6192
|
-
`
|
|
6193
|
-
`
|
|
6382
|
+
: probe.status === "diverged"
|
|
6383
|
+
? `\n\nCHECKED, and this session was NOT re-pointed: the live canvas is a ` +
|
|
6384
|
+
`DIFFERENT workflow (${probe.uuid}) than the one this command was issued ` +
|
|
6385
|
+
`for${stamped ? ` (${stamped})` : ""}. The fence is unchanged, so later ` +
|
|
6386
|
+
`edits in this sequence keep being refused rather than landing on the ` +
|
|
6387
|
+
`wrong canvas. WHAT TO DO: to edit the workflow you issued for, bring it ` +
|
|
6388
|
+
`back with panel_open_workflow; to follow the live canvas instead, ` +
|
|
6389
|
+
`re-target deliberately with panel_set_workflow_target({mode:"current"}). ` +
|
|
6390
|
+
`Either way the move is explicit — it is never made for you off a refused ` +
|
|
6391
|
+
`mutation.`
|
|
6392
|
+
: probe.status === "healed_by_panel"
|
|
6393
|
+
? `\n\nCHECKED, and the answer CHANGED while it was being read: the panel ` +
|
|
6394
|
+
`re-advertised its identity and this session's fence moved to the live ` +
|
|
6395
|
+
`canvas (${probe.uuid}) — through the panel's own repair, not this ` +
|
|
6396
|
+
`check. If you meant the EARLIER workflow, re-select it with ` +
|
|
6397
|
+
`panel_open_workflow before any further edits; they now target the live one.`
|
|
6398
|
+
: `\n\nCHECKED, but the live canvas could not be established ` +
|
|
6399
|
+
`(${probe.status}), so the fence is unchanged and a bare retry will fail ` +
|
|
6400
|
+
`the same way. Re-select the workflow you mean with panel_open_workflow, ` +
|
|
6401
|
+
`then retry.`;
|
|
6194
6402
|
}
|
|
6195
|
-
catch (
|
|
6403
|
+
catch (probeErr) {
|
|
6196
6404
|
// Never let the diagnosis fail the call differently than it already failed.
|
|
6197
6405
|
verdict =
|
|
6198
|
-
`\n\
|
|
6199
|
-
`stands on its own terms
|
|
6406
|
+
`\n\nCHECKED, and the check itself threw, so the live canvas is UNKNOWN — this ` +
|
|
6407
|
+
`refusal stands on its own terms and the fence is unchanged. ` +
|
|
6408
|
+
`(${probeErr instanceof Error ? probeErr.message : String(probeErr)})`;
|
|
6200
6409
|
}
|
|
6201
6410
|
return fail(`${name} was NOT applied — nothing changed. ${raw}${verdict}`);
|
|
6202
6411
|
}
|
|
@@ -8596,7 +8805,7 @@ export function buildPanelToolDefs() {
|
|
|
8596
8805
|
: "") +
|
|
8597
8806
|
`Any values in this result are the canvas's actual state.`);
|
|
8598
8807
|
}),
|
|
8599
|
-
def("panel_run", "Queue the workflow the user has OPEN — exactly like them pressing Queue Prompt (current widget values, the live graph they can see). On success it confirms the run was queued; if ComfyUI REFUSES the prompt (validation failure on either channel — per-node node_errors OR a top-level error like a missing node type) it returns a FAILURE with that rejection detail, never a false 'queued'. Pass to_node_id to RUN ONLY ONE BRANCH ('run to node'): ComfyUI renders just that output node plus everything upstream of it and SKIPS every other output branch — handy for previewing or debugging part of a big graph without rendering the whole thing. to_node_id MUST be an OUTPUT node (SaveImage, PreviewImage, SaveVideo, …) — pick the one at the END of the branch you want; nodes are tagged is_output:true in panel_query_graph's detail rows. The output node may be NESTED inside a subgraph — just pass its id (resolved in the scope you're currently viewing, then anywhere in the workflow); the tool builds the nested execution path for you. Omit it to run the whole graph. DUPLICATE FENCE (#862): if a render this session cannot account for is already in flight (after a reconnect this is usually YOUR earlier render still running — the queue record does not survive a restart), the run is REFUSED before anything is queued and the in-flight prompt is named; inspect queue (action:'list') first,
|
|
8808
|
+
def("panel_run", "Queue the workflow the user has OPEN — exactly like them pressing Queue Prompt (current widget values, the live graph they can see). On success it confirms the run was queued; if ComfyUI REFUSES the prompt (validation failure on either channel — per-node node_errors OR a top-level error like a missing node type) it returns a FAILURE with that rejection detail, never a false 'queued'. Pass to_node_id to RUN ONLY ONE BRANCH ('run to node'): ComfyUI renders just that output node plus everything upstream of it and SKIPS every other output branch — handy for previewing or debugging part of a big graph without rendering the whole thing. to_node_id MUST be an OUTPUT node (SaveImage, PreviewImage, SaveVideo, …) — pick the one at the END of the branch you want; nodes are tagged is_output:true in panel_query_graph's detail rows. The output node may be NESTED inside a subgraph — just pass its id (resolved in the scope you're currently viewing, then anywhere in the workflow); the tool builds the nested execution path for you. Omit it to run the whole graph. DUPLICATE FENCE (#862): if a render this session cannot account for is already in flight (after a reconnect this is usually YOUR earlier render still running — the queue record does not survive a restart), the run is REFUSED before anything is queued and the in-flight prompt is named; inspect queue (action:'list') first, then pass allow_duplicate:true once you have decided it is fine to run behind what is there — a scoped to_node_id preview after a reconnect is the ordinary case for it, a deliberate sweep/batch the other. Use this so the render runs on THEIR canvas and they see the result.", {
|
|
8600
8809
|
batch_count: z
|
|
8601
8810
|
.number()
|
|
8602
8811
|
.int()
|
|
@@ -8612,7 +8821,7 @@ export function buildPanelToolDefs() {
|
|
|
8612
8821
|
allow_duplicate: z
|
|
8613
8822
|
.boolean()
|
|
8614
8823
|
.optional()
|
|
8615
|
-
.describe("Queue even when a render this session cannot account for is already in flight (default false). When work is in flight that this session has no record of queueing — e.g. YOUR OWN earlier render still running after a reconnect, whose record does not survive the restart — panel_run REFUSES to stack a duplicate and names the in-flight prompt instead. Pass true
|
|
8824
|
+
.describe("Queue even when a render this session cannot account for is already in flight (default false). When work is in flight that this session has no record of queueing — e.g. YOUR OWN earlier render still running after a reconnect, whose record does not survive the restart — panel_run REFUSES to stack a duplicate and names the in-flight prompt instead. Pass true once you have LOOKED at what is in flight (queue action:'list') and decided it is fine to run behind it. After a reconnect that is the ordinary case, not an exotic one: you confirmed the in-flight job is your own earlier render or the user's, and you still want the next run — a scoped to_node_id preview, the next step of the task. Deliberately stacking a sweep/batch uses the same override."),
|
|
8616
8825
|
}, async (args, ctx) => {
|
|
8617
8826
|
// BACKPRESSURE: the agent can't see ComfyUI's queue, so re-queuing while a
|
|
8618
8827
|
// render is already running silently stacks behind it (this is how a stuck
|
|
@@ -8678,9 +8887,12 @@ export function buildPanelToolDefs() {
|
|
|
8678
8887
|
`no prompt id) even YOUR OWN earlier render reads as unconfirmable, and queueing now ` +
|
|
8679
8888
|
`would stack a DUPLICATE behind it (#862). Nothing was queued. Inspect with queue ` +
|
|
8680
8889
|
`(action:"list"): if the in-flight job is the render you already started, wait for it ` +
|
|
8681
|
-
`and confirm the outcome with get_history instead of re-running it.
|
|
8682
|
-
`
|
|
8683
|
-
`
|
|
8890
|
+
`and confirm the outcome with get_history instead of re-running it. Once you HAVE ` +
|
|
8891
|
+
`looked and decided it is fine to run behind what is there, re-call panel_run with ` +
|
|
8892
|
+
`allow_duplicate:true — after a reconnect that is the ORDINARY case, not an exotic ` +
|
|
8893
|
+
`one: the in-flight job is your own earlier render or the user's, and you still want ` +
|
|
8894
|
+
`the next run (a scoped to_node_id preview, the next step of the task). Deliberately ` +
|
|
8895
|
+
`stacking a sweep/batch uses the same override. If the in-flight job is actually wedged, queue ` +
|
|
8684
8896
|
`(action:"cancel") with clear_pending:true interrupts it AND drops everything pending.`);
|
|
8685
8897
|
}
|
|
8686
8898
|
const runCmd = { cmd: "graph_run", batch_count: args.batch_count, to_node_id: args.to_node_id };
|
|
@@ -11582,7 +11794,17 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
11582
11794
|
".") + argvNote + (preflightNote ? ` ${preflightNote}` : ""),
|
|
11583
11795
|
});
|
|
11584
11796
|
}),
|
|
11585
|
-
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.", {}, async (_args, ctx) =>
|
|
11797
|
+
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) => {
|
|
11798
|
+
const res = await ctx.call({ cmd: "free_vram" }, 15000);
|
|
11799
|
+
// #1249 — ONLY a no-reply is settled server-side. An acked executor error
|
|
11800
|
+
// (the panel's own "Failed to free VRAM: …") is a reply the bridge
|
|
11801
|
+
// received and relayed; it already says what failed, and re-issuing from
|
|
11802
|
+
// out here would fire a second mutation behind a verdict the caller was
|
|
11803
|
+
// given. A tagged reply-timeout is the one case where nothing answered.
|
|
11804
|
+
if (!isReplyTimeoutResult(res))
|
|
11805
|
+
return res;
|
|
11806
|
+
return settleFreeVramAfterAckTimeout(ctx, res);
|
|
11807
|
+
}),
|
|
11586
11808
|
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. NEVER describe an image with emoji or text placeholders — call this tool instead.", {
|
|
11587
11809
|
items: z
|
|
11588
11810
|
.array(z.object({
|