comfyui-mcp 0.52.29 → 0.52.30
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 +78 -0
- package/dist/orchestrator/node-id.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +215 -55
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/env-capabilities.js +41 -5
- package/dist/services/env-capabilities.js.map +1 -1
- package/dist/services/panel-recovery.js +52 -18
- package/dist/services/panel-recovery.js.map +1 -1
- package/dist/services/ui-bridge.js +51 -10
- package/dist/services/ui-bridge.js.map +1 -1
- package/package.json +1 -1
|
@@ -76,4 +76,82 @@ export function normalizeNodeId(v) {
|
|
|
76
76
|
/** The message a rejected id gets. Names the qualified shape explicitly, because a
|
|
77
77
|
* caller holding `263:78` needs to know whether it is unsupported or malformed. */
|
|
78
78
|
export const NODE_ID_MESSAGE = "a node id must be an integer (e.g. 42) or a subgraph-qualified id (e.g. 120:104)";
|
|
79
|
+
/**
|
|
80
|
+
* #1889 — a bare `z.union` renders as the word `Invalid input` and nothing else.
|
|
81
|
+
*
|
|
82
|
+
* Zod 4 reports a failed union as ONE `invalid_union` issue whose own `message`
|
|
83
|
+
* is the constant `"Invalid input"`; the per-member messages (NODE_ID_MESSAGE
|
|
84
|
+
* among them) live in a nested `errors` array. Every renderer that reaches an
|
|
85
|
+
* agent flattens `issue.message` and drops that nest — the MCP SDK's
|
|
86
|
+
* `getParseErrorMessage` (1.30) joins `` `${i.message} at ${path}` ``, zod's own
|
|
87
|
+
* `prettifyError` prints `✖ ${i.message}`, and 1.29 JSON-stringifies the issues.
|
|
88
|
+
* So a node-id rejection read:
|
|
89
|
+
*
|
|
90
|
+
* Invalid input at node_id
|
|
91
|
+
* Invalid input: expected string, received undefined at title
|
|
92
|
+
*
|
|
93
|
+
* NODE_ID_MESSAGE was never dead prose — a BAD STRING (`"42px"`) fails only the
|
|
94
|
+
* string member, zod collapses that to a plain `invalid_format` issue, and the
|
|
95
|
+
* message prints. It is unreachable only for inputs that fail EVERY member
|
|
96
|
+
* (undefined, null, 4.5, an object), which is the common case and the reported
|
|
97
|
+
* one. This is what makes the union say the same thing there.
|
|
98
|
+
*
|
|
99
|
+
* `received` is carried too, because the complaint was the asymmetry with the
|
|
100
|
+
* sibling `title`, and half of what `title` said was what it actually got.
|
|
101
|
+
*/
|
|
102
|
+
export function unionErrorFor(expected) {
|
|
103
|
+
return (iss) => `${expected}; received ${describeReceived(iss.input)}`;
|
|
104
|
+
}
|
|
105
|
+
/** How long a rendered value may get before it is cut. A node id is a handful of
|
|
106
|
+
* characters; anything near this is a caller error worth showing, not quoting. */
|
|
107
|
+
const RECEIVED_MAX = 60;
|
|
108
|
+
/**
|
|
109
|
+
* Render an arbitrary rejected input for a message, TOTALLY.
|
|
110
|
+
*
|
|
111
|
+
* This runs inside zod's error path, so it must never throw: a formatter that
|
|
112
|
+
* throws converts a clean validation refusal into a 500. `JSON.stringify` alone
|
|
113
|
+
* is not safe here — it throws on a BigInt and on a circular object, returns the
|
|
114
|
+
* bare `undefined` value for a symbol or a function, and renders NaN/Infinity as
|
|
115
|
+
* `null`, which would tell a caller who passed NaN that they passed null. Each
|
|
116
|
+
* of those is handled before the stringify, and the stringify itself is caught.
|
|
117
|
+
*/
|
|
118
|
+
export function describeReceived(input) {
|
|
119
|
+
if (input === null)
|
|
120
|
+
return "null";
|
|
121
|
+
switch (typeof input) {
|
|
122
|
+
case "undefined":
|
|
123
|
+
return "undefined";
|
|
124
|
+
case "bigint":
|
|
125
|
+
return `${input}n`;
|
|
126
|
+
case "symbol":
|
|
127
|
+
return input.toString();
|
|
128
|
+
case "function":
|
|
129
|
+
return "a function";
|
|
130
|
+
case "number":
|
|
131
|
+
case "boolean":
|
|
132
|
+
// `String` and not `JSON.stringify`: the latter renders NaN and ±Infinity
|
|
133
|
+
// as `null`, which would tell a caller who passed NaN that they passed null.
|
|
134
|
+
return String(input);
|
|
135
|
+
default:
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
let rendered;
|
|
139
|
+
try {
|
|
140
|
+
rendered = JSON.stringify(input) ?? String(input);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// The fallback needs its own guard: `Array.isArray` THROWS on a revoked
|
|
144
|
+
// proxy ("Cannot perform 'IsArray' on a proxy that has been revoked"), so
|
|
145
|
+
// the naive recovery path was itself a way for this function to throw.
|
|
146
|
+
// Measured, not assumed — it is the one hostile input of ten that got past
|
|
147
|
+
// the outer catch.
|
|
148
|
+
try {
|
|
149
|
+
return Array.isArray(input) ? "an array" : "an object";
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return "an object";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return rendered.length > RECEIVED_MAX ? `${rendered.slice(0, RECEIVED_MAX)}…` : rendered;
|
|
156
|
+
}
|
|
79
157
|
//# sourceMappingURL=node-id.js.map
|
|
@@ -1 +1 @@
|
|
|
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"}
|
|
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;AAErF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,QAAQ,cAAc,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;AACzE,CAAC;AAED;mFACmF;AACnF,MAAM,YAAY,GAAG,EAAE,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,QAAQ,OAAO,KAAK,EAAE,CAAC;QACrB,KAAK,WAAW;YACd,OAAO,WAAW,CAAC;QACrB,KAAK,QAAQ;YACX,OAAO,GAAG,KAAK,GAAG,CAAC;QACrB,KAAK,QAAQ;YACX,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC1B,KAAK,UAAU;YACb,OAAO,YAAY,CAAC;QACtB,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS;YACZ,0EAA0E;YAC1E,6EAA6E;YAC7E,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;QACvB;YACE,MAAM;IACV,CAAC;IACD,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,0EAA0E;QAC1E,uEAAuE;QACvE,2EAA2E;QAC3E,mBAAmB;QACnB,IAAI,CAAC;YACH,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,WAAW,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC3F,CAAC"}
|
|
@@ -46,7 +46,7 @@ import { compareSemver } from "../services/self-update.js";
|
|
|
46
46
|
import { describeInstallPanelAction } from "../services/panel-recovery.js";
|
|
47
47
|
import { peekResolvedPanelBase, primePanelBase, verifiedPanelDiskVersion, } from "../services/panel-workspace.js";
|
|
48
48
|
import { conversationOfScopeAddress, isScopeAddress, shortTabId } from "../services/session-scope.js";
|
|
49
|
-
import { NODE_ID_MESSAGE, NODE_ID_PATTERN, normalizeNodeId, PLAIN_NODE_ID_PATTERN, } from "./node-id.js";
|
|
49
|
+
import { NODE_ID_MESSAGE, NODE_ID_PATTERN, normalizeNodeId, PLAIN_NODE_ID_PATTERN, unionErrorFor, } from "./node-id.js";
|
|
50
50
|
import { parseContradictoryPromotedWidgetRefusal, resolveInnerPromotedTarget, } from "./promoted-widget.js";
|
|
51
51
|
import { includeRequestedCreateGroupMembers } from "./create-group-membership.js";
|
|
52
52
|
import { fastGroupsFilterPropertyNote, isFastGroupsFilterProperty, } from "./rgthree-fast-groups-property.js";
|
|
@@ -78,7 +78,7 @@ function journalTabFor(ctx) {
|
|
|
78
78
|
function journalConversationFor(ctx) {
|
|
79
79
|
return conversationOfScopeAddress(ctx.tabId);
|
|
80
80
|
}
|
|
81
|
-
import { BRIDGE_DEFAULT_TIMEOUT_MS, BRIDGE_READ_DEFAULT_TIMEOUT_MS, dispatchOutcomeOf, GRAPH_CMD_EFFECT, isCapabilityRefusal, isPanelCmdUnsupportedError, isReplyTimeoutTagged, isRoutingAmbiguity, requiresWorkflowStampEnforcement, } from "../services/ui-bridge.js";
|
|
81
|
+
import { BRIDGE_DEFAULT_TIMEOUT_MS, BRIDGE_READ_DEFAULT_TIMEOUT_MS, dispatchOutcomeOf, GRAPH_CMD_EFFECT, isCapabilityRefusal, isPanelCmdUnsupportedError, isMidCommandDisconnectTagged, isReplyTimeoutTagged, isRoutingAmbiguity, requiresWorkflowStampEnforcement, } from "../services/ui-bridge.js";
|
|
82
82
|
import { withWorkflowTarget, } from "../services/workflow-target-store.js";
|
|
83
83
|
import { addUserMcpServer, readUserMcpServers, removeUserMcpServer, setUserMcpServerSecret, } from "../services/user-mcp-config.js";
|
|
84
84
|
import { setComfyuiSecret, setAgentSecret, isAllowedAgentSecretKey, receiptDisclosures, shadowedNote, storeDamageNote, } from "../services/panel-secrets.js";
|
|
@@ -580,6 +580,10 @@ export const __panelToolsTestHooks = {
|
|
|
580
580
|
setReadVramDevices(fn) {
|
|
581
581
|
readVramDevicesOverride = fn;
|
|
582
582
|
},
|
|
583
|
+
/** #1895 tests drive these, not a copy. A mutation of either that the
|
|
584
|
+
* tool-handler suite never reaches must still fail. */
|
|
585
|
+
annotateFreeVramAck,
|
|
586
|
+
deviceStillPinned,
|
|
583
587
|
/** Direct access to the #742 decline recheck loop so its hard-deadline
|
|
584
588
|
* guarantee (codex gate r2) can be unit-tested with a custom deadline. */
|
|
585
589
|
probeDeclineRecovery,
|
|
@@ -2075,6 +2079,27 @@ function carryReplyTimeoutMark(err, res) {
|
|
|
2075
2079
|
function isReplyTimeoutResult(res) {
|
|
2076
2080
|
return res?.isError === true && res[REPLY_TIMEOUT_RESULT] === true;
|
|
2077
2081
|
}
|
|
2082
|
+
/** panel#1524 — carry the bridge's typed mid-command-disconnect marker onto the
|
|
2083
|
+
* ToolResult the same way reply-timeout is carried: an acked panel error can
|
|
2084
|
+
* quote "OUTCOME UNKNOWN", so the reconcile must not key on message text. */
|
|
2085
|
+
const MID_COMMAND_DISCONNECT_RESULT = Symbol("panel.midCommandDisconnectResult");
|
|
2086
|
+
function carryMidCommandDisconnectMark(err, res) {
|
|
2087
|
+
if (!isMidCommandDisconnectTagged(err))
|
|
2088
|
+
return res;
|
|
2089
|
+
Object.defineProperty(res, MID_COMMAND_DISCONNECT_RESULT, {
|
|
2090
|
+
value: true,
|
|
2091
|
+
enumerable: false,
|
|
2092
|
+
configurable: true,
|
|
2093
|
+
});
|
|
2094
|
+
return res;
|
|
2095
|
+
}
|
|
2096
|
+
function isMidCommandDisconnectResult(res) {
|
|
2097
|
+
return (res?.isError === true && res[MID_COMMAND_DISCONNECT_RESULT] === true);
|
|
2098
|
+
}
|
|
2099
|
+
/** A `graph_run` we wrote and then lost the reply for — timeout or disconnect. */
|
|
2100
|
+
function isUnackedGraphRunResult(res) {
|
|
2101
|
+
return isReplyTimeoutResult(res) || isMidCommandDisconnectResult(res);
|
|
2102
|
+
}
|
|
2078
2103
|
/**
|
|
2079
2104
|
* #1560 — the same translation problem, for the OTHER fact: did anything answer?
|
|
2080
2105
|
*
|
|
@@ -2291,6 +2316,11 @@ function sampleVramDevice(d) {
|
|
|
2291
2316
|
* H3 case was device 2 with the torch pool at ~0.24% free — that occupancy
|
|
2292
2317
|
* is what this threshold names.
|
|
2293
2318
|
*
|
|
2319
|
+
* The ratio is pool FULLNESS, not the reserved share of the card
|
|
2320
|
+
* (`torch_vram_total / vram_total`). A 5 GiB reservation with 4 GiB unused
|
|
2321
|
+
* inside the pool is not pinned here — that shape was not observed live
|
|
2322
|
+
* after /free (#1895 item 3).
|
|
2323
|
+
*
|
|
2294
2324
|
* Unknown/unreadable torch counters are NOT pinned: an unknown answer
|
|
2295
2325
|
* claims nothing in either direction (#1473). Device-global counters
|
|
2296
2326
|
* alone also claim nothing — they cannot tell ComfyUI from another process. */
|
|
@@ -2312,11 +2342,28 @@ function deviceStillPinned(d) {
|
|
|
2312
2342
|
function pinnedVramDevices(devices) {
|
|
2313
2343
|
return devices.filter(deviceStillPinned);
|
|
2314
2344
|
}
|
|
2315
|
-
/**
|
|
2316
|
-
*
|
|
2317
|
-
* occupied
|
|
2318
|
-
|
|
2319
|
-
|
|
2345
|
+
/** Device-global occupancy (`vram_free` / `vram_total`). Same 1 GiB / 20%
|
|
2346
|
+
* thresholds as the torch pin, applied to the WHOLE card. Used to disclose
|
|
2347
|
+
* an occupied card that is not this ComfyUI's torch pin (#1895) — never to
|
|
2348
|
+
* flip freed:true to a /free failure. */
|
|
2349
|
+
function deviceOccupiedGlobally(d) {
|
|
2350
|
+
const total = d.vram_total;
|
|
2351
|
+
const free = d.vram_free;
|
|
2352
|
+
if (typeof total !== "number" || typeof free !== "number")
|
|
2353
|
+
return false;
|
|
2354
|
+
if (!Number.isFinite(total) || !Number.isFinite(free))
|
|
2355
|
+
return false;
|
|
2356
|
+
if (total < PINNED_VRAM_MIN_TOTAL_BYTES)
|
|
2357
|
+
return false;
|
|
2358
|
+
if (free < 0)
|
|
2359
|
+
return true;
|
|
2360
|
+
return free / total <= PINNED_VRAM_FREE_RATIO;
|
|
2361
|
+
}
|
|
2362
|
+
function occupiedVramDevices(devices) {
|
|
2363
|
+
return devices.filter(deviceOccupiedGlobally);
|
|
2364
|
+
}
|
|
2365
|
+
function nameVramDevices(devices) {
|
|
2366
|
+
return devices
|
|
2320
2367
|
.map((d) => {
|
|
2321
2368
|
if (typeof d.index === "number")
|
|
2322
2369
|
return `device ${d.index}`;
|
|
@@ -2325,12 +2372,45 @@ function pinnedVramAfterFreeNote(pinned) {
|
|
|
2325
2372
|
return "an unnamed device";
|
|
2326
2373
|
})
|
|
2327
2374
|
.join(", ");
|
|
2375
|
+
}
|
|
2376
|
+
/** /free only unloads ComfyUI's model manager. Ray workers, parallel CLIP
|
|
2377
|
+
* loaders, and other custom-node allocations survive it and keep the GPU
|
|
2378
|
+
* occupied — which is why a 2xx from /free is not evidence VRAM is free. */
|
|
2379
|
+
function pinnedVramAfterFreeNote(pinned) {
|
|
2328
2380
|
return (`ComfyUI /free accepted (unload_models + free_memory) but VRAM is STILL PINNED on ` +
|
|
2329
|
-
`${
|
|
2381
|
+
`${nameVramDevices(pinned)}. /free only unloads ComfyUI's model manager — it does not terminate Ray ` +
|
|
2330
2382
|
`workers or custom-node allocations (Raylight sequence-parallel, parallel CLIP loaders). ` +
|
|
2331
2383
|
`This is not a successful VRAM recovery. Next: panel_restart_comfyui (last resort; ` +
|
|
2332
2384
|
`refuses mid-render) after confirming with get_system_stats (action:"stats").`);
|
|
2333
2385
|
}
|
|
2386
|
+
/** #1895 — the free DID succeed (torch pool not pinned) but the card is still
|
|
2387
|
+
* occupied. /system_stats is device-global and cannot name the holder, so
|
|
2388
|
+
* the wording asserts neither (a) this ComfyUI outside torch nor (b) another
|
|
2389
|
+
* process. */
|
|
2390
|
+
function occupiedCardAfterFreeNote(occupied) {
|
|
2391
|
+
const named = nameVramDevices(occupied);
|
|
2392
|
+
const verb = occupied.length === 1 ? "is" : "are";
|
|
2393
|
+
return (`ComfyUI /free accepted and THIS instance's torch pool is not pinned, so the free DID succeed. ` +
|
|
2394
|
+
`${named} ${verb} still occupied. /system_stats reports the device globally and cannot name ` +
|
|
2395
|
+
`which process holds it. Two shapes this reading cannot distinguish: (a) allocations this ` +
|
|
2396
|
+
`ComfyUI made outside torch's allocator (Ray workers, sequence-parallel / parallel CLIP ` +
|
|
2397
|
+
`loaders) — panel_restart_comfyui of THIS instance can clear those; (b) a different process ` +
|
|
2398
|
+
`on the host (a second ComfyUI, a trainer, the browser) — restarting THIS instance will not ` +
|
|
2399
|
+
`free that VRAM.`);
|
|
2400
|
+
}
|
|
2401
|
+
function occupiedCardAfterFreeResult(occupied, extra) {
|
|
2402
|
+
return ok({
|
|
2403
|
+
freed: true,
|
|
2404
|
+
unload_models: true,
|
|
2405
|
+
free_memory: true,
|
|
2406
|
+
model_manager_freed: true,
|
|
2407
|
+
occupied_devices: occupied,
|
|
2408
|
+
...extra,
|
|
2409
|
+
note: occupiedCardAfterFreeNote(occupied),
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
const UNOBSERVED_OCCUPANCY_NOTE = `freed:true is the panel's /free receipt, not a measured device — this tab's server could ` +
|
|
2413
|
+
`not be proven as the local boot instance, so /system_stats was never re-read.`;
|
|
2334
2414
|
function pinnedVramAfterFreeResult(pinned, extra) {
|
|
2335
2415
|
return {
|
|
2336
2416
|
content: [
|
|
@@ -2437,11 +2517,17 @@ async function settleFreeVramAfterAckTimeout(ctx, timedOut) {
|
|
|
2437
2517
|
};
|
|
2438
2518
|
// #1866 — a 2xx from /free is not a free GPU. Ray/CLIP workers can keep a
|
|
2439
2519
|
// device pinned. When we have occupancy numbers, they are the verdict.
|
|
2520
|
+
// #1895 — a card that is occupied while THIS instance's torch pool is empty
|
|
2521
|
+
// is not a /free failure; disclose the device instead of a bare freed:true.
|
|
2440
2522
|
if (direct.after != null) {
|
|
2441
2523
|
const pinned = pinnedVramDevices(direct.after);
|
|
2442
2524
|
if (pinned.length > 0) {
|
|
2443
2525
|
return pinnedVramAfterFreeResult(pinned, extra);
|
|
2444
2526
|
}
|
|
2527
|
+
const occupied = occupiedVramDevices(direct.after);
|
|
2528
|
+
if (occupied.length > 0) {
|
|
2529
|
+
return occupiedCardAfterFreeResult(occupied, extra);
|
|
2530
|
+
}
|
|
2445
2531
|
}
|
|
2446
2532
|
const statsNote = direct.before != null && direct.after != null
|
|
2447
2533
|
? "vram_before/vram_after are the server's own /system_stats counters around the free."
|
|
@@ -2468,7 +2554,12 @@ async function settleFreeVramAfterAckTimeout(ctx, timedOut) {
|
|
|
2468
2554
|
* settle uses (`captureRebootHealthBase(ctx)`), never `getComfyUIBaseUrl()`.
|
|
2469
2555
|
* A hello from another tab can retarget the global base asynchronously;
|
|
2470
2556
|
* reporting that GPU as this command's failure is the wrong-target failure
|
|
2471
|
-
* the gate exists to prevent.
|
|
2557
|
+
* the gate exists to prevent.
|
|
2558
|
+
*
|
|
2559
|
+
* #1895 — no proven local server must not look like a measured GPU: disclose
|
|
2560
|
+
* that occupancy was never re-read. An occupied card whose torch pool is
|
|
2561
|
+
* empty is not a /free failure (isError stays false, freed stays true) but
|
|
2562
|
+
* MUST name the device and that /system_stats is device-global.
|
|
2472
2563
|
*/
|
|
2473
2564
|
async function annotateFreeVramAck(ctx, res) {
|
|
2474
2565
|
if (res.isError)
|
|
@@ -2477,18 +2568,31 @@ async function annotateFreeVramAck(ctx, res) {
|
|
|
2477
2568
|
if (!parsed || parsed.freed !== true)
|
|
2478
2569
|
return res;
|
|
2479
2570
|
const base = captureRebootHealthBase(ctx);
|
|
2480
|
-
if (!base)
|
|
2481
|
-
return
|
|
2571
|
+
if (!base) {
|
|
2572
|
+
return ok({
|
|
2573
|
+
...parsed,
|
|
2574
|
+
occupancy_reread: false,
|
|
2575
|
+
note: UNOBSERVED_OCCUPANCY_NOTE,
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2482
2578
|
const devices = await readVramDevicesMaybe(base, FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
2483
2579
|
if (devices == null)
|
|
2484
2580
|
return res;
|
|
2485
2581
|
const pinned = pinnedVramDevices(devices);
|
|
2486
|
-
if (pinned.length
|
|
2487
|
-
return
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}
|
|
2582
|
+
if (pinned.length > 0) {
|
|
2583
|
+
return pinnedVramAfterFreeResult(pinned, {
|
|
2584
|
+
acknowledged: true,
|
|
2585
|
+
devices,
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
const occupied = occupiedVramDevices(devices);
|
|
2589
|
+
if (occupied.length > 0) {
|
|
2590
|
+
return occupiedCardAfterFreeResult(occupied, {
|
|
2591
|
+
acknowledged: true,
|
|
2592
|
+
devices,
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
return res;
|
|
2492
2596
|
}
|
|
2493
2597
|
// ---- panel_install_node: accepted-but-never-enqueued (#1129) ---------------
|
|
2494
2598
|
// #1143 fixed the pre-queue REFUSAL (403/404 → direct clone). This is the other
|
|
@@ -8094,7 +8198,7 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
8094
8198
|
const res = await callOnce(cmd, timeoutMs, onDispatchedRid, (err) => {
|
|
8095
8199
|
failure = err;
|
|
8096
8200
|
});
|
|
8097
|
-
return carryPanelAnsweredMark(failure, res);
|
|
8201
|
+
return carryMidCommandDisconnectMark(failure, carryPanelAnsweredMark(failure, res));
|
|
8098
8202
|
};
|
|
8099
8203
|
// Human-in-the-loop confirmation for a DESTRUCTIVE op: render a yes/no card in
|
|
8100
8204
|
// the panel and block on the user's pick. Returns false on decline, timeout, or
|
|
@@ -8804,45 +8908,85 @@ function runLateAckGraceMs() {
|
|
|
8804
8908
|
return runLateAckGraceMsOverride ?? RUN_LATE_ACK_GRACE_MS;
|
|
8805
8909
|
}
|
|
8806
8910
|
/**
|
|
8807
|
-
* Wait, bounded, for
|
|
8808
|
-
*
|
|
8809
|
-
*
|
|
8810
|
-
*
|
|
8811
|
-
*
|
|
8812
|
-
*
|
|
8813
|
-
*
|
|
8814
|
-
*
|
|
8815
|
-
*
|
|
8816
|
-
*
|
|
8817
|
-
*
|
|
8818
|
-
*
|
|
8819
|
-
*
|
|
8820
|
-
*
|
|
8821
|
-
*
|
|
8822
|
-
*
|
|
8823
|
-
*
|
|
8911
|
+
* Wait, bounded, for evidence that a `graph_run` we stopped waiting for still
|
|
8912
|
+
* queued — and hand back the reply it would have produced on time (#1175,
|
|
8913
|
+
* panel#1524).
|
|
8914
|
+
*
|
|
8915
|
+
* Two receipts, in this order:
|
|
8916
|
+
*
|
|
8917
|
+
* 1. The panel's own late `graph_run` body, retained by the bridge after a
|
|
8918
|
+
* reply-timeout (#1175) or a mid-command disconnect (panel#1524). Stronger:
|
|
8919
|
+
* it is the executor's answer, rid-correlated.
|
|
8920
|
+
* 2. A prompt id that appeared in ComfyUI's queue AFTER this dispatch, which
|
|
8921
|
+
* was not the in-flight prompt when we sent `graph_run`. Weaker: inferred
|
|
8922
|
+
* from the watchdog, not the panel. Used when the tab dropped before
|
|
8923
|
+
* acknowledging a paid render that is observably running.
|
|
8924
|
+
*
|
|
8925
|
+
* Returns null when there is nothing to reconcile. The negatives are deliberate:
|
|
8926
|
+
*
|
|
8927
|
+
* - NOT an unacked post-write (timeout or disconnect) ⇒ nothing to wait for.
|
|
8928
|
+
* Gated on the bridge's own typed marks, never on message text: an acked
|
|
8929
|
+
* panel error can quote our timeout/OUTCOME UNKNOWN sentence verbatim
|
|
8930
|
+
* (#1468 round 2), and the difference is whether the tab answered, which
|
|
8931
|
+
* only the bridge knows.
|
|
8932
|
+
* - NO retained body AND no new queue prompt ⇒ leave any unusable late-mutation
|
|
8933
|
+
* entry ALONE. The retry-token layer drains this same rid to tell the caller
|
|
8934
|
+
* their earlier attempt landed; consuming it here to answer a question we
|
|
8935
|
+
* then could not act on would delete the recovery the caller still had.
|
|
8936
|
+
* Hence peek-then-take.
|
|
8937
|
+
* - A DIFFERENT command's entry ⇒ leave it alone for the same reason.
|
|
8938
|
+
* - The SAME prompt that was already running at dispatch ⇒ not this command's
|
|
8939
|
+
* receipt. Claiming it would launder a prior render as this run.
|
|
8824
8940
|
*/
|
|
8825
|
-
|
|
8826
|
-
|
|
8941
|
+
function promptAppearedAfterDispatch(preRunningPromptId) {
|
|
8942
|
+
const id = QueueMonitor.snapshot().runningPromptId;
|
|
8943
|
+
if (typeof id !== "string")
|
|
8827
8944
|
return null;
|
|
8828
|
-
const
|
|
8829
|
-
if (
|
|
8945
|
+
const now = id.trim();
|
|
8946
|
+
if (now === "")
|
|
8830
8947
|
return null;
|
|
8831
|
-
|
|
8948
|
+
const pre = typeof preRunningPromptId === "string" && preRunningPromptId.trim() !== ""
|
|
8949
|
+
? preRunningPromptId.trim()
|
|
8950
|
+
: null;
|
|
8951
|
+
if (pre !== null && now === pre)
|
|
8952
|
+
return null;
|
|
8953
|
+
return now;
|
|
8954
|
+
}
|
|
8955
|
+
async function reconcileLateRunAck(ctx, res, rid, preRunningPromptId) {
|
|
8956
|
+
if (!isUnackedGraphRunResult(res))
|
|
8832
8957
|
return null;
|
|
8833
|
-
const
|
|
8958
|
+
const fromQueue = () => {
|
|
8959
|
+
const promptId = promptAppearedAfterDispatch(preRunningPromptId);
|
|
8960
|
+
if (!promptId)
|
|
8961
|
+
return null;
|
|
8962
|
+
return { result: { queued: true, prompt_id: promptId }, lateByMs: 0, via: "queue" };
|
|
8963
|
+
};
|
|
8964
|
+
const bridge = ctx.bridge;
|
|
8965
|
+
const canPeek = Boolean(rid) &&
|
|
8966
|
+
typeof bridge?.peekLateMutation === "function" &&
|
|
8967
|
+
typeof bridge?.takeLateMutation === "function";
|
|
8968
|
+
// Stubbed bridges in unit tests have no late-mutation map. One-shot the queue
|
|
8969
|
+
// (no grace wait) so a timeout whose prompt did not change still returns in
|
|
8970
|
+
// the same tick it always has.
|
|
8971
|
+
if (!canPeek)
|
|
8972
|
+
return fromQueue();
|
|
8973
|
+
const startedAt = Date.now();
|
|
8974
|
+
const deadline = startedAt + runLateAckGraceMs();
|
|
8834
8975
|
for (;;) {
|
|
8835
8976
|
const seen = bridge.peekLateMutation(rid);
|
|
8836
8977
|
if (seen) {
|
|
8837
8978
|
if (seen.cmd !== "graph_run" || !("result" in seen))
|
|
8838
|
-
return
|
|
8979
|
+
return fromQueue();
|
|
8839
8980
|
// Only now is it certain this entry will be USED, so draining it costs the
|
|
8840
8981
|
// caller nothing: they are about to be handed its contents.
|
|
8841
8982
|
const taken = bridge.takeLateMutation(rid);
|
|
8842
8983
|
if (!taken || !("result" in taken))
|
|
8843
|
-
return
|
|
8844
|
-
return { result: taken.result, lateByMs: taken.lateByMs };
|
|
8984
|
+
return fromQueue();
|
|
8985
|
+
return { result: taken.result, lateByMs: taken.lateByMs, via: "ack" };
|
|
8845
8986
|
}
|
|
8987
|
+
const queued = fromQueue();
|
|
8988
|
+
if (queued)
|
|
8989
|
+
return { ...queued, lateByMs: Date.now() - startedAt };
|
|
8846
8990
|
const left = deadline - Date.now();
|
|
8847
8991
|
if (left <= 0)
|
|
8848
8992
|
return null;
|
|
@@ -8861,6 +9005,15 @@ function lateRunAckNote(lateByMs) {
|
|
|
8861
9005
|
`this is its outcome. (A queue listing taken in the moments after a run is accepted can still ` +
|
|
8862
9006
|
`be empty, so an empty queue would not have settled this either way.)`);
|
|
8863
9007
|
}
|
|
9008
|
+
/** panel#1524 — the panel never acknowledged, but the queue holds a prompt this
|
|
9009
|
+
* dispatch created. Names the id and says we did not re-issue. */
|
|
9010
|
+
function queueRunReceiptNote(promptId) {
|
|
9011
|
+
return (`\n\n[RECOVERED] The panel tab disconnected (or missed its ack) before acknowledging this ` +
|
|
9012
|
+
`graph_run, but ComfyUI's queue now contains prompt ${promptId} — which was not the in-flight ` +
|
|
9013
|
+
`prompt when this command was dispatched. Treating that as this run's receipt (queued:true). ` +
|
|
9014
|
+
`Nothing was dispatched a second time. Do NOT re-run: a second panel_run would bill/queue ` +
|
|
9015
|
+
`another render behind the one already running.`);
|
|
9016
|
+
}
|
|
8864
9017
|
async function pollLateAskReply(bridge, askId, timing, hardDeadline) {
|
|
8865
9018
|
const take = bridge
|
|
8866
9019
|
.takeLateAskReply;
|
|
@@ -9252,10 +9405,12 @@ function validatePanelEditNodeArgs(args) {
|
|
|
9252
9405
|
* node-id.ts, where truncating `"263:78"` to `263` would edit the wrong node.
|
|
9253
9406
|
*/
|
|
9254
9407
|
const nodeId = () => z
|
|
9255
|
-
.
|
|
9256
|
-
|
|
9257
|
-
|
|
9258
|
-
]
|
|
9408
|
+
// #1889 — the union-level `error` is what a caller actually reads. Without it
|
|
9409
|
+
// zod names the failed union `"Invalid input"` and buries NODE_ID_MESSAGE in a
|
|
9410
|
+
// nested `errors` array that no renderer prints. See unionErrorFor.
|
|
9411
|
+
.union([z.number().int(), z.string().regex(NODE_ID_PATTERN, NODE_ID_MESSAGE)], {
|
|
9412
|
+
error: unionErrorFor(NODE_ID_MESSAGE),
|
|
9413
|
+
})
|
|
9259
9414
|
.transform(normalizeNodeId);
|
|
9260
9415
|
/**
|
|
9261
9416
|
* #1497 — the ONE node-id argument #845 never reached: panel_run's `to_node_id`.
|
|
@@ -9284,10 +9439,10 @@ const nodeId = () => z
|
|
|
9284
9439
|
*/
|
|
9285
9440
|
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';
|
|
9286
9441
|
const runToNodeId = () => z
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
])
|
|
9442
|
+
// #1889, same as nodeId(): #1497 wrote RUN_TO_NODE_ID_MESSAGE so a qualified id
|
|
9443
|
+
// would be "refused now by name, with a reason" — and for the shapes that fail
|
|
9444
|
+
// BOTH members the name and the reason were dropped on the floor.
|
|
9445
|
+
.union([z.number().int(), z.string().regex(PLAIN_NODE_ID_PATTERN, RUN_TO_NODE_ID_MESSAGE)], { error: unionErrorFor(RUN_TO_NODE_ID_MESSAGE) })
|
|
9291
9446
|
.transform((v) => (typeof v === "number" ? v : Number.parseInt(v, 10)));
|
|
9292
9447
|
/**
|
|
9293
9448
|
* #845 — which `panel_canvas` arguments the chosen action actually consumes.
|
|
@@ -10674,11 +10829,16 @@ export function buildPanelToolDefs() {
|
|
|
10674
10829
|
// guidance. Rebuilding the reply here (rather than describing it) is what
|
|
10675
10830
|
// makes that possible — `ok()` is exactly what ctx.call would have produced.
|
|
10676
10831
|
const reconcileRun = async () => {
|
|
10677
|
-
const recovered = await reconcileLateRunAck(ctx, res, runRid);
|
|
10832
|
+
const recovered = await reconcileLateRunAck(ctx, res, runRid, pre.runningPromptId);
|
|
10678
10833
|
if (!recovered)
|
|
10679
10834
|
return;
|
|
10680
10835
|
res = ok(recovered.result);
|
|
10681
|
-
lateAckNote =
|
|
10836
|
+
lateAckNote =
|
|
10837
|
+
recovered.via === "queue"
|
|
10838
|
+
? queueRunReceiptNote(typeof recovered.result.prompt_id === "string"
|
|
10839
|
+
? recovered.result.prompt_id
|
|
10840
|
+
: "?")
|
|
10841
|
+
: lateRunAckNote(recovered.lateByMs);
|
|
10682
10842
|
};
|
|
10683
10843
|
await reconcileRun();
|
|
10684
10844
|
// Derive the verdict from the AUTHORITATIVE reply, not a bare `queued`
|
|
@@ -13999,7 +14159,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13999
14159
|
".") + argvNote + (preflightNote ? ` ${preflightNote}` : ""),
|
|
14000
14160
|
});
|
|
14001
14161
|
}),
|
|
14002
|
-
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, THIS instance's torch-pool occupancy is re-read from /system_stats on the server this tab provably fronts: 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. Device-global occupancy
|
|
14162
|
+
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, THIS instance's torch-pool occupancy is re-read from /system_stats on the server this tab provably fronts: 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. Device-global occupancy with an empty torch pool is not this free failing (freed:true) but the reply still names the device and that /system_stats cannot say which process holds the card. If this tab's server cannot be proven, freed:true is the panel's /free receipt — occupancy was not re-read. 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) => {
|
|
14003
14163
|
const res = await ctx.call({ cmd: "free_vram" }, 15000);
|
|
14004
14164
|
// #1249 — ONLY a no-reply is settled server-side. An acked executor error
|
|
14005
14165
|
// (the panel's own "Failed to free VRAM: …") is a reply the bridge
|