comfyui-mcp 0.52.28 → 0.52.29
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/create-group-membership.js +263 -0
- package/dist/orchestrator/create-group-membership.js.map +1 -0
- package/dist/orchestrator/panel-tools.js +163 -32
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/download-jobs.js +30 -3
- package/dist/services/download-jobs.js.map +1 -1
- package/dist/services/download-progress.js +67 -10
- package/dist/services/download-progress.js.map +1 -1
- package/dist/services/listener-ownership.js +4 -1
- package/dist/services/listener-ownership.js.map +1 -1
- package/dist/services/manager-update-error.js +15 -8
- package/dist/services/manager-update-error.js.map +1 -1
- package/dist/services/process-control.js +280 -7
- package/dist/services/process-control.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1877 — panel_create_group can wrap a collapsed node and still report it
|
|
3
|
+
* missing.
|
|
4
|
+
*
|
|
5
|
+
* The panel sizes the box from live `pos`/`size`. A collapsed node often has
|
|
6
|
+
* `size[1] === 0` (title-chip only), so the box is ~100px tall around the
|
|
7
|
+
* node's origin. Membership then tests the CENTRE of a boundingRect rebuilt
|
|
8
|
+
* with the panel's `finiteExtent` fallback (0 is not a usable height, so the
|
|
9
|
+
* body becomes 100px plus a 30px title). That centre sits a few pixels below
|
|
10
|
+
* the box, so the group is empty even though the node's coordinates are inside.
|
|
11
|
+
*
|
|
12
|
+
* These helpers expand a created group's bounds just enough to contain every
|
|
13
|
+
* membership-plausible centre of a requested node whose origin is already in
|
|
14
|
+
* the box — then the caller writes those bounds with graph_edit_group.
|
|
15
|
+
*
|
|
16
|
+
* Constants match comfyui-mcp-panel `web/js/lib/group-geometry.js`
|
|
17
|
+
* (`COLLAPSED_TITLE_HEIGHT`, `COLLAPSED_PILL_WIDTH`, `finiteExtent` fallbacks)
|
|
18
|
+
* and LiteGraph's containsCentre rule (min edge inclusive, max edge exclusive).
|
|
19
|
+
*/
|
|
20
|
+
export const NODE_TITLE_HEIGHT = 30;
|
|
21
|
+
export const COLLAPSED_PILL_WIDTH = 80;
|
|
22
|
+
export const DEFAULT_NODE_WIDTH = 200;
|
|
23
|
+
export const DEFAULT_NODE_BODY_HEIGHT = 100;
|
|
24
|
+
function finiteExtent(value, fallback) {
|
|
25
|
+
const n = Number(value);
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
27
|
+
}
|
|
28
|
+
function finiteQuad(v) {
|
|
29
|
+
if (!Array.isArray(v) || v.length !== 4)
|
|
30
|
+
return null;
|
|
31
|
+
const q = [Number(v[0]), Number(v[1]), Number(v[2]), Number(v[3])];
|
|
32
|
+
return q.every(Number.isFinite) ? q : null;
|
|
33
|
+
}
|
|
34
|
+
export function nodePosInsideGroupBounds(pos, bounds) {
|
|
35
|
+
const [gx, gy, gw, gh] = bounds;
|
|
36
|
+
const [x, y] = pos;
|
|
37
|
+
// Inclusive on every edge: the reporter's "bounds cover the node" is a
|
|
38
|
+
// top-left origin sitting on or inside the box, not a centre test.
|
|
39
|
+
return x >= gx && x <= gx + gw && y >= gy && y <= gy + gh;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Centres LiteGraph / the panel might use for containsCentre, given only the
|
|
43
|
+
* geometry panel_query_graph actually returns (pos, size, collapsed, full_height).
|
|
44
|
+
*/
|
|
45
|
+
export function membershipCandidateCenters(node) {
|
|
46
|
+
const x = node.pos[0];
|
|
47
|
+
const y = node.pos[1];
|
|
48
|
+
const pillW = finiteExtent(node.size[0], COLLAPSED_PILL_WIDTH);
|
|
49
|
+
const fullW = finiteExtent(node.size[0], DEFAULT_NODE_WIDTH);
|
|
50
|
+
const fullH = finiteExtent(node.size[1], DEFAULT_NODE_BODY_HEIGHT);
|
|
51
|
+
const chipH = finiteExtent(node.full_height, NODE_TITLE_HEIGHT);
|
|
52
|
+
return [
|
|
53
|
+
// Collapsed title-chip (syncNodeArea without forceCollapsed).
|
|
54
|
+
{ x: x + pillW / 2, y: y - NODE_TITLE_HEIGHT + chipH / 2 },
|
|
55
|
+
// forceCollapsed wantedNodeArea: size[1]===0 falls back to DEFAULT_NODE_BODY_HEIGHT.
|
|
56
|
+
{ x: x + fullW / 2, y: y - NODE_TITLE_HEIGHT + (fullH + NODE_TITLE_HEIGHT) / 2 },
|
|
57
|
+
// Raw pos + stored size (degenerate when size[1] is 0 — origin itself).
|
|
58
|
+
{ x: x + fullW / 2, y: y + (node.size[1] > 0 ? node.size[1] : 0) / 2 },
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
/** Grow `bounds` so every point is a containsCentre member (max edge exclusive). */
|
|
62
|
+
export function expandBoundsToContainCenters(bounds, points) {
|
|
63
|
+
let [x, y, w, h] = bounds;
|
|
64
|
+
let x2 = x + w;
|
|
65
|
+
let y2 = y + h;
|
|
66
|
+
for (const p of points) {
|
|
67
|
+
if (!Number.isFinite(p.x) || !Number.isFinite(p.y))
|
|
68
|
+
continue;
|
|
69
|
+
if (p.x < x)
|
|
70
|
+
x = p.x;
|
|
71
|
+
if (p.y < y)
|
|
72
|
+
y = p.y;
|
|
73
|
+
if (p.x >= x2)
|
|
74
|
+
x2 = p.x + 1;
|
|
75
|
+
if (p.y >= y2)
|
|
76
|
+
y2 = p.y + 1;
|
|
77
|
+
}
|
|
78
|
+
return [x, y, x2 - x, y2 - y];
|
|
79
|
+
}
|
|
80
|
+
export function expandGroupBoundsForMembership(bounds, nodes) {
|
|
81
|
+
const points = [];
|
|
82
|
+
for (const n of nodes) {
|
|
83
|
+
if (!nodePosInsideGroupBounds(n.pos, bounds))
|
|
84
|
+
continue;
|
|
85
|
+
points.push(...membershipCandidateCenters(n));
|
|
86
|
+
}
|
|
87
|
+
return points.length ? expandBoundsToContainCenters(bounds, points) : bounds;
|
|
88
|
+
}
|
|
89
|
+
function idKey(id) {
|
|
90
|
+
return String(id);
|
|
91
|
+
}
|
|
92
|
+
export function classifyRequestedMembership(requested, members) {
|
|
93
|
+
const reqKeys = new Set(requested.map(idKey));
|
|
94
|
+
const memberKeys = new Set(members.map(idKey));
|
|
95
|
+
return {
|
|
96
|
+
extra: members.filter((id) => !reqKeys.has(idKey(id))),
|
|
97
|
+
missing: requested.filter((id) => !memberKeys.has(idKey(id))),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function asIdList(v) {
|
|
101
|
+
if (!Array.isArray(v))
|
|
102
|
+
return [];
|
|
103
|
+
return v.filter((id) => typeof id === "number" || typeof id === "string");
|
|
104
|
+
}
|
|
105
|
+
function parseToolJson(res) {
|
|
106
|
+
if (!res || res.isError)
|
|
107
|
+
return null;
|
|
108
|
+
const text = res.content.find((c) => c.type === "text")?.text;
|
|
109
|
+
if (typeof text !== "string")
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(text);
|
|
113
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
114
|
+
? parsed
|
|
115
|
+
: null;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function withPayload(res, payload) {
|
|
122
|
+
const first = res.content[0];
|
|
123
|
+
if (!first || first.type !== "text")
|
|
124
|
+
return res;
|
|
125
|
+
return {
|
|
126
|
+
...res,
|
|
127
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }, ...res.content.slice(1)],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function groupFromCreateReply(payload) {
|
|
131
|
+
const inner = payload.group;
|
|
132
|
+
if (inner && typeof inner === "object" && !Array.isArray(inner))
|
|
133
|
+
return inner;
|
|
134
|
+
if ("bounding" in payload || "node_ids" in payload)
|
|
135
|
+
return payload;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
export function parseDetailNodesFromQueryText(text) {
|
|
139
|
+
if (typeof text !== "string")
|
|
140
|
+
return [];
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const line of text.split("\n")) {
|
|
143
|
+
const s = line.trim();
|
|
144
|
+
if (!s.startsWith("{"))
|
|
145
|
+
continue;
|
|
146
|
+
try {
|
|
147
|
+
const row = JSON.parse(s);
|
|
148
|
+
const id = row.id;
|
|
149
|
+
if (typeof id !== "number" && typeof id !== "string")
|
|
150
|
+
continue;
|
|
151
|
+
const pos = row.pos;
|
|
152
|
+
const size = row.size;
|
|
153
|
+
if (!Array.isArray(pos) || pos.length < 2 || !Array.isArray(size) || size.length < 2)
|
|
154
|
+
continue;
|
|
155
|
+
const x = Number(pos[0]);
|
|
156
|
+
const y = Number(pos[1]);
|
|
157
|
+
const w = Number(size[0]);
|
|
158
|
+
const h = Number(size[1]);
|
|
159
|
+
if (![x, y, w, h].every(Number.isFinite))
|
|
160
|
+
continue;
|
|
161
|
+
const geom = { id, pos: [x, y], size: [w, h] };
|
|
162
|
+
if (row.collapsed === true)
|
|
163
|
+
geom.collapsed = true;
|
|
164
|
+
if (typeof row.full_height === "number" && Number.isFinite(row.full_height)) {
|
|
165
|
+
geom.full_height = row.full_height;
|
|
166
|
+
}
|
|
167
|
+
out.push(geom);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* skip a non-JSON detail line */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function honestyWarning(extra, missing) {
|
|
176
|
+
return ("group membership is geometric: the box that wraps the requested nodes " +
|
|
177
|
+
`also captures ${extra} unrelated node(s) (their centre falls inside)` +
|
|
178
|
+
(missing ? ` and misses ${missing} requested node(s)` : "") +
|
|
179
|
+
". Move the intended nodes into a contiguous region (panel_edit_node / " +
|
|
180
|
+
"panel_auto_layout) before grouping, or edit the group bounds, to get an exact set.");
|
|
181
|
+
}
|
|
182
|
+
function attachHonesty(group, requested) {
|
|
183
|
+
const live = asIdList(group.node_ids);
|
|
184
|
+
const { extra, missing } = classifyRequestedMembership(requested, live);
|
|
185
|
+
const out = { ...group, requested_node_ids: requested };
|
|
186
|
+
delete out.extra_node_ids;
|
|
187
|
+
delete out.missing_node_ids;
|
|
188
|
+
delete out.warning;
|
|
189
|
+
if (extra.length || missing.length) {
|
|
190
|
+
if (extra.length)
|
|
191
|
+
out.extra_node_ids = extra;
|
|
192
|
+
if (missing.length)
|
|
193
|
+
out.missing_node_ids = missing;
|
|
194
|
+
out.warning = honestyWarning(extra.length, missing.length);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function quadsEqual(a, b) {
|
|
199
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* After graph_create_group, if requested ids are missing AND their origin sits
|
|
203
|
+
* inside the new box, expand the box so those centres are members and rewrite
|
|
204
|
+
* the reply. Never throws: a failed repair returns the original create result.
|
|
205
|
+
*/
|
|
206
|
+
export async function includeRequestedCreateGroupMembers(created, requestedIds, call) {
|
|
207
|
+
try {
|
|
208
|
+
if (created.isError)
|
|
209
|
+
return created;
|
|
210
|
+
const requested = asIdList(requestedIds);
|
|
211
|
+
if (!requested.length)
|
|
212
|
+
return created;
|
|
213
|
+
const payload = parseToolJson(created);
|
|
214
|
+
if (!payload)
|
|
215
|
+
return created;
|
|
216
|
+
const group = groupFromCreateReply(payload);
|
|
217
|
+
if (!group)
|
|
218
|
+
return created;
|
|
219
|
+
const bounds = finiteQuad(group.bounding);
|
|
220
|
+
if (!bounds)
|
|
221
|
+
return created;
|
|
222
|
+
const live = asIdList(group.node_ids);
|
|
223
|
+
const reportedMissing = asIdList(group.missing_node_ids);
|
|
224
|
+
const missing = reportedMissing.length
|
|
225
|
+
? reportedMissing.filter((id) => requested.some((r) => idKey(r) === idKey(id)))
|
|
226
|
+
: classifyRequestedMembership(requested, live).missing;
|
|
227
|
+
if (!missing.length)
|
|
228
|
+
return created;
|
|
229
|
+
const query = await call({
|
|
230
|
+
cmd: "graph_query",
|
|
231
|
+
ids: missing,
|
|
232
|
+
fields: "detail",
|
|
233
|
+
limit: Math.min(Math.max(missing.length, 1), 200),
|
|
234
|
+
}, 8000);
|
|
235
|
+
if (query.isError)
|
|
236
|
+
return created;
|
|
237
|
+
const qPayload = parseToolJson(query);
|
|
238
|
+
const nodes = parseDetailNodesFromQueryText(qPayload?.text).filter((n) => missing.some((id) => idKey(id) === idKey(n.id)));
|
|
239
|
+
const repairable = nodes.filter((n) => nodePosInsideGroupBounds(n.pos, bounds));
|
|
240
|
+
if (!repairable.length)
|
|
241
|
+
return created;
|
|
242
|
+
const expanded = expandGroupBoundsForMembership(bounds, repairable);
|
|
243
|
+
if (quadsEqual(expanded, bounds))
|
|
244
|
+
return created;
|
|
245
|
+
const groupId = group.id;
|
|
246
|
+
if (typeof groupId !== "number" && typeof groupId !== "string")
|
|
247
|
+
return created;
|
|
248
|
+
const edited = await call({ cmd: "graph_edit_group", group_id: groupId, bounds: expanded }, 15000);
|
|
249
|
+
if (edited.isError)
|
|
250
|
+
return created;
|
|
251
|
+
const ePayload = parseToolJson(edited);
|
|
252
|
+
if (!ePayload)
|
|
253
|
+
return created;
|
|
254
|
+
const editedGroup = groupFromCreateReply(ePayload);
|
|
255
|
+
if (!editedGroup)
|
|
256
|
+
return created;
|
|
257
|
+
return withPayload(created, { ...ePayload, group: attachHonesty(editedGroup, requested) });
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return created;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=create-group-membership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-group-membership.js","sourceRoot":"","sources":["../../src/orchestrator/create-group-membership.ts"],"names":[],"mappings":"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"}
|
|
@@ -48,6 +48,7 @@ import { peekResolvedPanelBase, primePanelBase, verifiedPanelDiskVersion, } from
|
|
|
48
48
|
import { conversationOfScopeAddress, isScopeAddress, shortTabId } from "../services/session-scope.js";
|
|
49
49
|
import { NODE_ID_MESSAGE, NODE_ID_PATTERN, normalizeNodeId, PLAIN_NODE_ID_PATTERN, } from "./node-id.js";
|
|
50
50
|
import { parseContradictoryPromotedWidgetRefusal, resolveInnerPromotedTarget, } from "./promoted-widget.js";
|
|
51
|
+
import { includeRequestedCreateGroupMembers } from "./create-group-membership.js";
|
|
51
52
|
import { fastGroupsFilterPropertyNote, isFastGroupsFilterProperty, } from "./rgthree-fast-groups-property.js";
|
|
52
53
|
import { isPlainObject, isStampMismatchSaveRefusal, openLiveMatchesDestContent, patchOpenIdentity, shouldRebindOpenIdentity, workflowFromSerializeReply, } from "./open-identity-normalization.js";
|
|
53
54
|
import { clearSwitchHold, describeSwitchHold, recordSwitchHold, successProvesSwitchCleared, } from "./switch-hold.js";
|
|
@@ -662,12 +663,47 @@ function sleep(ms) {
|
|
|
662
663
|
// frozen/backgrounded tab fails in bounded time instead of hanging forever.
|
|
663
664
|
const OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS = 30_000;
|
|
664
665
|
// #1639 — while a ComfyUI prompt is running the frontend main thread often
|
|
665
|
-
// cannot service graph_* at all
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
666
|
+
// cannot service graph_* at all. Waiting out the 20/30 s ack bound only
|
|
667
|
+
// surfaces "tab may be backgrounded or frozen" with an unknown mutation
|
|
668
|
+
// outcome. Fail closed BEFORE dispatch for canvas-touching graph commands so
|
|
669
|
+
// the agent gets an explicit QUEUE BUSY instead. `graph_run` is excluded:
|
|
670
|
+
// queuing behind an in-flight job is the documented sweep path, and panel_run
|
|
671
|
+
// already has its own duplicate fence.
|
|
672
|
+
//
|
|
673
|
+
// panel#1489 — that fence was applied to READS too, and it should not have been.
|
|
674
|
+
//
|
|
675
|
+
// The harm #1639 names is specific to a WRITE: a delivered frame cannot be
|
|
676
|
+
// retracted, so a write that times out mid-render leaves the caller unable to say
|
|
677
|
+
// whether it applied. A read carries no such outcome. Abandoning one costs a
|
|
678
|
+
// retry and nothing else — the asymmetry BRIDGE_DEFAULT_TIMEOUT_MS's own doc
|
|
679
|
+
// spells out ("a read abandoned too early costs a retry; a write abandoned too
|
|
680
|
+
// early is UNRECOVERABLE ambiguity"). So fencing reads bought no safety at all,
|
|
681
|
+
// and it cost the reporter the thing they needed: panel_graph_outline on a
|
|
682
|
+
// running render was rejected unsent, forcing queue-polling and a later retry
|
|
683
|
+
// just to LOOK at the graph.
|
|
684
|
+
//
|
|
685
|
+
// It also asserted more than we know, in BOTH directions. "The tab typically
|
|
686
|
+
// cannot answer" is a prediction, not a measurement — and refusing the call made
|
|
687
|
+
// it unfalsifiable, because no read ever got to try. The architectural reason to
|
|
688
|
+
// doubt it is that ComfyUI executes in server-side Python, so the browser main
|
|
689
|
+
// thread is not the thing doing the work; how often a real tab answers mid-render
|
|
690
|
+
// is NOT measured here, and nothing below depends on a number for it. Attempting
|
|
691
|
+
// the read is what turns the question back into something observable.
|
|
692
|
+
//
|
|
693
|
+
// Note what #1639 actually asked for: "(a) keep the panel's command channel
|
|
694
|
+
// responsive during execution (the reads at minimum should never be blocked by a
|
|
695
|
+
// running prompt), or (b) ... say so explicitly instead of a generic timeout."
|
|
696
|
+
// It shipped (b) and applied it to reads as well, which is the one combination
|
|
697
|
+
// the reporter did not ask for. Reads now get BOTH halves: the call is attempted,
|
|
698
|
+
// and if the tab really cannot answer, queueBusyTimeoutNote still names the
|
|
699
|
+
// running prompt instead of surfacing a bare "backgrounded or frozen".
|
|
700
|
+
//
|
|
701
|
+
// Reads keep their normal tolerant ack budget on purpose. Shortening it mid-render
|
|
702
|
+
// is tempting — a frozen tab costs the caller 20 s instead of 0 — but that is the
|
|
703
|
+
// trade #357 and #589 were both regressions of, in a file whose read-timeout policy
|
|
704
|
+
// is "reads get MORE patience, not less, because a false timeout costs an agent its
|
|
705
|
+
// only look at a broken graph". #589 is precisely this: panel_get_errors was given a
|
|
706
|
+
// 30 s budget because the panel's own bound is 18 s. A cap here would re-break it.
|
|
671
707
|
function queueBusySnapshotNote() {
|
|
672
708
|
const snap = QueueMonitor.snapshot();
|
|
673
709
|
if (!snap.running)
|
|
@@ -677,26 +713,54 @@ function queueBusySnapshotNote() {
|
|
|
677
713
|
const close = snap.runningPromptId ? ")" : "";
|
|
678
714
|
return `${prompt}${node}${close}`;
|
|
679
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* panel#1489 — is this `graph_*` command one the queue-busy fence must let past?
|
|
718
|
+
*
|
|
719
|
+
* Answered from GRAPH_CMD_EFFECT, which is the ledger that classifies a graph
|
|
720
|
+
* command by exactly the property this gate turns on: `targeted` means "changes
|
|
721
|
+
* workflow content, publishes from it, or queues a render of it — a delivered
|
|
722
|
+
* frame cannot be retracted", which IS the unknown-outcome hazard #1639 fenced.
|
|
723
|
+
* `inert` means it cannot change workflow content and cannot act on it, so a
|
|
724
|
+
* frame delivered into a busy tab has nothing to leave half-done.
|
|
725
|
+
*
|
|
726
|
+
* NOT `BRIDGE_READONLY_CMDS`: that set answers re-dispatch safety (which default
|
|
727
|
+
* timeout, may a mid-command socket drop be parked), and #778 is the standing
|
|
728
|
+
* record of what reading one list for the other question costs — `graph_canvas`
|
|
729
|
+
* is inert but not idempotent, `refresh_nodes` is idempotent but not a read.
|
|
730
|
+
*
|
|
731
|
+
* Fail-closed by construction: an unlisted command has no entry, `undefined` is
|
|
732
|
+
* not `"inert"`, and it stays fenced. A new mutator is never waved past this gate
|
|
733
|
+
* by omission.
|
|
734
|
+
*/
|
|
735
|
+
function graphCmdIsInertUnderQueueBusy(name) {
|
|
736
|
+
return GRAPH_CMD_EFFECT[name] === "inert";
|
|
737
|
+
}
|
|
680
738
|
function graphCmdBlockedByRunningPrompt(cmd) {
|
|
681
739
|
const name = typeof cmd.cmd === "string" ? cmd.cmd : "";
|
|
682
740
|
if (!name.startsWith("graph_") || name === "graph_run")
|
|
683
741
|
return null;
|
|
742
|
+
// panel#1489 — reads and view/selection changes go through. Only a command
|
|
743
|
+
// that could leave the workflow half-changed is worth refusing unsent.
|
|
744
|
+
if (graphCmdIsInertUnderQueueBusy(name))
|
|
745
|
+
return null;
|
|
684
746
|
const snap = QueueMonitor.snapshot();
|
|
685
747
|
if (!snap.running)
|
|
686
748
|
return null;
|
|
687
749
|
return (`${name} was NOT sent — nothing was applied. QUEUE BUSY: a ComfyUI prompt is running` +
|
|
688
|
-
`${queueBusySnapshotNote()}.
|
|
689
|
-
`
|
|
690
|
-
`
|
|
691
|
-
`
|
|
750
|
+
`${queueBusySnapshotNote()}. ${name} MUTATES the workflow, and the panel tab often ` +
|
|
751
|
+
`cannot service a graph edit while a prompt is executing — delivering it would only ` +
|
|
752
|
+
`surface a generic "tab may be backgrounded or frozen" timeout with an unknown ` +
|
|
753
|
+
`outcome, leaving you unable to say whether the edit applied. Read-only graph calls ` +
|
|
754
|
+
`(graph_outline / graph_query / graph_get_errors) are NOT fenced and can be used right ` +
|
|
755
|
+
`now to inspect the graph. Retry this edit after queue (action:"list") shows running: 0.`);
|
|
692
756
|
}
|
|
693
757
|
function queueBusyTimeoutNote() {
|
|
694
758
|
if (!QueueMonitor.snapshot().running)
|
|
695
759
|
return "";
|
|
696
|
-
return (`\n\nQUEUE BUSY: a ComfyUI prompt is still running${queueBusySnapshotNote()}
|
|
697
|
-
`
|
|
698
|
-
`
|
|
699
|
-
`
|
|
760
|
+
return (`\n\nQUEUE BUSY: a ComfyUI prompt is still running${queueBusySnapshotNote()}, which is the ` +
|
|
761
|
+
`most likely reason the tab did not answer in time — a render can occupy the panel's main ` +
|
|
762
|
+
`thread. Retry after queue (action:"list") shows running: 0; if it still does not answer ` +
|
|
763
|
+
`once the queue is idle, THEN treat the tab as backgrounded or frozen.`);
|
|
700
764
|
}
|
|
701
765
|
const RETRY_SAFE_CMDS = new Set([
|
|
702
766
|
// Idempotent reads (mirror UiBridge.READONLY_CMDS + list/status probes).
|
|
@@ -2217,19 +2281,24 @@ function sampleVramDevice(d) {
|
|
|
2217
2281
|
sample.torch_vram_free = dev.torch_vram_free;
|
|
2218
2282
|
return sample;
|
|
2219
2283
|
}
|
|
2220
|
-
/** A device
|
|
2221
|
-
*
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2284
|
+
/** A device whose ComfyUI torch pool is at least 1 GiB is still PINNED when
|
|
2285
|
+
* less than 20% of that pool is free after /free. `vram_free` is
|
|
2286
|
+
* torch.cuda.mem_get_info — the whole device, every process — so it cannot
|
|
2287
|
+
* attribute a pin to THIS ComfyUI. `torch_vram_total` / `torch_vram_free`
|
|
2288
|
+
* are this process's allocator: a 32 MiB leftover pool next to a card
|
|
2289
|
+
* occupied by another instance is not a /free failure and must not
|
|
2290
|
+
* prescribe panel_restart_comfyui (#1887). The reporter's Raylight MiniMax
|
|
2291
|
+
* H3 case was device 2 with the torch pool at ~0.24% free — that occupancy
|
|
2292
|
+
* is what this threshold names.
|
|
2293
|
+
*
|
|
2294
|
+
* Unknown/unreadable torch counters are NOT pinned: an unknown answer
|
|
2295
|
+
* claims nothing in either direction (#1473). Device-global counters
|
|
2296
|
+
* alone also claim nothing — they cannot tell ComfyUI from another process. */
|
|
2228
2297
|
const PINNED_VRAM_MIN_TOTAL_BYTES = 1024 * 1024 * 1024;
|
|
2229
2298
|
const PINNED_VRAM_FREE_RATIO = 0.2;
|
|
2230
2299
|
function deviceStillPinned(d) {
|
|
2231
|
-
const total = d.
|
|
2232
|
-
const free = d.
|
|
2300
|
+
const total = d.torch_vram_total;
|
|
2301
|
+
const free = d.torch_vram_free;
|
|
2233
2302
|
if (typeof total !== "number" || typeof free !== "number")
|
|
2234
2303
|
return false;
|
|
2235
2304
|
if (!Number.isFinite(total) || !Number.isFinite(free))
|
|
@@ -2394,14 +2463,20 @@ async function settleFreeVramAfterAckTimeout(ctx, timedOut) {
|
|
|
2394
2463
|
* GPU. After the tab says it posted /free, re-read /system_stats and refuse to
|
|
2395
2464
|
* claim VRAM was freed when a device is still occupied. Unreadable stats leave
|
|
2396
2465
|
* the original ack UNTOUCHED: an unknown answer claims nothing extra.
|
|
2466
|
+
*
|
|
2467
|
+
* #1887 — occupancy is read from the same proven local base the frozen-tab
|
|
2468
|
+
* settle uses (`captureRebootHealthBase(ctx)`), never `getComfyUIBaseUrl()`.
|
|
2469
|
+
* A hello from another tab can retarget the global base asynchronously;
|
|
2470
|
+
* reporting that GPU as this command's failure is the wrong-target failure
|
|
2471
|
+
* the gate exists to prevent. No proven local server → leave the ack.
|
|
2397
2472
|
*/
|
|
2398
|
-
async function annotateFreeVramAck(res) {
|
|
2473
|
+
async function annotateFreeVramAck(ctx, res) {
|
|
2399
2474
|
if (res.isError)
|
|
2400
2475
|
return res;
|
|
2401
2476
|
const parsed = parseToolResultJson(res);
|
|
2402
2477
|
if (!parsed || parsed.freed !== true)
|
|
2403
2478
|
return res;
|
|
2404
|
-
const base = (
|
|
2479
|
+
const base = captureRebootHealthBase(ctx);
|
|
2405
2480
|
if (!base)
|
|
2406
2481
|
return res;
|
|
2407
2482
|
const devices = await readVramDevicesMaybe(base, FREE_VRAM_DIRECT_TIMEOUT_MS);
|
|
@@ -7151,9 +7226,11 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
7151
7226
|
await awaitReachable();
|
|
7152
7227
|
}
|
|
7153
7228
|
ensureReachable();
|
|
7154
|
-
// #1639 — a running prompt
|
|
7155
|
-
//
|
|
7229
|
+
// #1639 — a running prompt can freeze the tab's graph_* channel. Refuse a
|
|
7230
|
+
// MUTATION before dispatch so it is known-not-applied rather than
|
|
7156
7231
|
// delivered-into-a-frozen-tab with a 20/30s unknown-outcome timeout.
|
|
7232
|
+
// panel#1489 — reads are NOT refused here: they carry no unknown outcome,
|
|
7233
|
+
// so they are dispatched on the bounded budget above instead.
|
|
7157
7234
|
const blocked = graphCmdBlockedByRunningPrompt(cmd);
|
|
7158
7235
|
if (blocked)
|
|
7159
7236
|
return fail(blocked);
|
|
@@ -7202,6 +7279,38 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
7202
7279
|
let holdTab = ctx.tabId;
|
|
7203
7280
|
try {
|
|
7204
7281
|
await sleep(retrySettleMs());
|
|
7282
|
+
// #1881 — a retry-safe READ that failed on an EMPTY tab registry gets the
|
|
7283
|
+
// same bounded reconnect wait a mutating edit already gets, instead of
|
|
7284
|
+
// spending its one re-issue ~400ms in.
|
|
7285
|
+
//
|
|
7286
|
+
// #436 gated mutations here and deliberately left the read path alone, on
|
|
7287
|
+
// the reasoning that "a read survives that window (it is parked mid-command
|
|
7288
|
+
// and is retry-safe)". Parking is what happens to a command the bridge
|
|
7289
|
+
// ACCEPTED and cannot answer yet. This failure happens strictly earlier:
|
|
7290
|
+
// resolveTarget throws before any socket write (dispatched:false), so there
|
|
7291
|
+
// is no parked command — the read just gets one re-issue after the settle,
|
|
7292
|
+
// and reconnectWaitTiming's own docstring says why that loses: "the browser
|
|
7293
|
+
// reconnects its own socket seconds-to-tens-of-seconds after ComfyUI comes
|
|
7294
|
+
// back, so the existing single ~400ms retry always loses the race."
|
|
7295
|
+
//
|
|
7296
|
+
// Reported shape: after a full ComfyUI Desktop restart the panel's chat
|
|
7297
|
+
// frame is delivered (deliverPanelEvent runs whether or not the socket has
|
|
7298
|
+
// re-registered) while its `hello` — the ONLY writer of the connected-tab
|
|
7299
|
+
// registry — has not landed yet. So the turn is live, `Connected: none` is
|
|
7300
|
+
// true, and panel_graph_outline failed both immediately and 3s later; only
|
|
7301
|
+
// a manual browser refresh cleared it.
|
|
7302
|
+
//
|
|
7303
|
+
// awaitReachable() is SELF-GATING, which is the whole safety argument: it
|
|
7304
|
+
// returns at once when the bound tab is reachable, and it loops ONLY while
|
|
7305
|
+
// zero interactive tabs are connected. So this adds latency in exactly one
|
|
7306
|
+
// state — the genuine post-restart empty registry — and changes nothing for
|
|
7307
|
+
// a healthy session, a multi-tab session, or a panel that answered (a
|
|
7308
|
+
// pre-executor / workflow-switch refusal reaches here with its tab still
|
|
7309
|
+
// connected, so the wait is a no-op for those). The command is idempotent
|
|
7310
|
+
// and NOTHING was dispatched, so waiting cannot double-apply. Reads outside
|
|
7311
|
+
// RETRY_SAFE_CMDS never enter this branch at all, so graph_list_subgraphs
|
|
7312
|
+
// and training_get_state keep failing fast.
|
|
7313
|
+
await awaitReachable();
|
|
7205
7314
|
ensureReachable(); // rebinds a current-mode session onto the reconnected tab
|
|
7206
7315
|
holdTab = ctx.tabId;
|
|
7207
7316
|
const retried = ok(await sendRouted(cmd, timeoutMs, observeRid));
|
|
@@ -12392,14 +12501,17 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
12392
12501
|
.describe("Explicit [x, y, width, height] (four numbers). Ignored if node_ids is given."),
|
|
12393
12502
|
color: z.string().optional().describe("Box/header color, e.g. '#3f789e'."),
|
|
12394
12503
|
font_size: z.number().optional().describe("Title font size (default 24)."),
|
|
12395
|
-
},
|
|
12504
|
+
},
|
|
12505
|
+
// #1877 — a collapsed node can sit inside the auto-fit box and still be
|
|
12506
|
+
// reported missing (size[1]===0 vs membership's 100px body fallback).
|
|
12507
|
+
async (args, ctx) => includeRequestedCreateGroupMembers(await ctx.call({
|
|
12396
12508
|
cmd: "graph_create_group",
|
|
12397
12509
|
title: args.title,
|
|
12398
12510
|
node_ids: args.node_ids,
|
|
12399
12511
|
bounds: args.bounds,
|
|
12400
12512
|
color: args.color,
|
|
12401
12513
|
font_size: args.font_size,
|
|
12402
|
-
}, 15000)),
|
|
12514
|
+
}, 15000), args.node_ids, (cmd, timeoutMs) => ctx.call(cmd, timeoutMs))),
|
|
12403
12515
|
def("panel_move_group", "Move a group box to a new top-left [x, y] on the user's open graph. By default the nodes inside the group move with it (like dragging the group header); pass move_nodes:false to move only the box. Group id comes from panel_query_graph (the `groups` array on every result) or panel_create_group. Undoable.", {
|
|
12404
12516
|
group_id: z.number().int().describe("Group id from panel_query_graph's groups[] / panel_create_group."),
|
|
12405
12517
|
pos: xy().describe("New top-left [x, y] (two numbers)."),
|
|
@@ -13299,6 +13411,10 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13299
13411
|
// process tree was unreadable, so the launch signatures stood in). Appended
|
|
13300
13412
|
// to the outcome note below — a dispatch allowed on an inference says so.
|
|
13301
13413
|
let preflightNote;
|
|
13414
|
+
// #1847: proven python-command relaunch when Desktop parent inspection
|
|
13415
|
+
// could not identify a supervisor. Routes to Manager-stop, then spawn
|
|
13416
|
+
// only if that parent is gone — a live parent may already be relaunching.
|
|
13417
|
+
let preflightSelfRelaunch = false;
|
|
13302
13418
|
// The target generation as of BEFORE the preflight resolved that argv, so the
|
|
13303
13419
|
// whole span up to the post-restart reading sits inside one instance fence.
|
|
13304
13420
|
let preflightArgvGeneration = -1;
|
|
@@ -13328,6 +13444,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13328
13444
|
preflightArgv = preflight.observedArgv;
|
|
13329
13445
|
preflightIsDesktop = preflight.isDesktopApp === true;
|
|
13330
13446
|
preflightNote = preflight.note;
|
|
13447
|
+
preflightSelfRelaunch = preflight.selfRelaunch === true;
|
|
13331
13448
|
preflightArgvGeneration = preflightTargetGeneration;
|
|
13332
13449
|
// r8/r9/r10: the preflight AWAIT makes the pre-decision captures
|
|
13333
13450
|
// STALE — and the preflight itself reads MUTABLE config (target URL,
|
|
@@ -13450,6 +13567,20 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13450
13567
|
observedOrigin: ctx.bridge?.tabServerOrigin?.(ctx.tabId) ?? null,
|
|
13451
13568
|
}));
|
|
13452
13569
|
}
|
|
13570
|
+
// #1847: parent-process inspection could not identify a Desktop
|
|
13571
|
+
// supervisor, but the launch command is proven on disk. Manager-stop,
|
|
13572
|
+
// then spawn that command only if the parent is gone — a live parent
|
|
13573
|
+
// may already be relaunching, and a free port is not proof it isn't.
|
|
13574
|
+
if (preflightSelfRelaunch && healthBase != null && dispatchBound) {
|
|
13575
|
+
return runHeadlessManagedRestart({
|
|
13576
|
+
healthBase,
|
|
13577
|
+
preRestartPanelIdentity,
|
|
13578
|
+
why: "Desktop parent-process inspection could not identify a supervisor, and the launch command is proven on disk",
|
|
13579
|
+
mechanism: "a Manager stop, then the proven launch command only if that parent process is gone",
|
|
13580
|
+
noteHealthyLead: "Desktop parent-process inspection could not identify a supervisor; the proven launch command",
|
|
13581
|
+
noteRanLead: "Desktop parent-process inspection could not identify a supervisor; ran a Manager stop (and the proven launch command only if that parent process was gone)",
|
|
13582
|
+
});
|
|
13583
|
+
}
|
|
13453
13584
|
const timing = getPanelRebootTiming();
|
|
13454
13585
|
const dispatchTimeout = Math.max(1, Math.min(15000, overallDeadline - Date.now()));
|
|
13455
13586
|
// CONCURRENT OBSERVATION (coordinator): start probing the fixed boot endpoint NOW,
|
|
@@ -13868,7 +13999,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13868
13999
|
".") + argvNote + (preflightNote ? ` ${preflightNote}` : ""),
|
|
13869
14000
|
});
|
|
13870
14001
|
}),
|
|
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) => {
|
|
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 held by another process is not this free failing. 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) => {
|
|
13872
14003
|
const res = await ctx.call({ cmd: "free_vram" }, 15000);
|
|
13873
14004
|
// #1249 — ONLY a no-reply is settled server-side. An acked executor error
|
|
13874
14005
|
// (the panel's own "Failed to free VRAM: …") is a reply the bridge
|
|
@@ -13879,7 +14010,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13879
14010
|
return settleFreeVramAfterAckTimeout(ctx, res);
|
|
13880
14011
|
// #1866 — a successful /free ack is not a free GPU. Re-read occupancy
|
|
13881
14012
|
// and refuse to report freed:true when a device is still pinned.
|
|
13882
|
-
return annotateFreeVramAck(res);
|
|
14013
|
+
return annotateFreeVramAck(ctx, res);
|
|
13883
14014
|
}),
|
|
13884
14015
|
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.", {
|
|
13885
14016
|
items: z
|