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.
@@ -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 (reads included). Waiting out the 20/30 s ack
666
- // bound only surfaces "tab may be backgrounded or frozen" with an unknown
667
- // mutation outcome. Fail closed BEFORE dispatch for canvas-touching graph
668
- // commands so the agent gets an explicit QUEUE BUSY instead. `graph_run` is
669
- // excluded: queuing behind an in-flight job is the documented sweep path, and
670
- // panel_run already has its own duplicate fence.
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()}. The panel tab typically cannot answer graph_* commands ` +
689
- `(including read-only graph_query / graph_outline) while a prompt is executing — ` +
690
- `waiting out the ack timeout would only surface a generic "tab may be backgrounded ` +
691
- `or frozen" with an unknown outcome. Retry after queue (action:"list") shows running: 0.`);
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
- `The panel tab typically cannot answer graph_* (including read-only queries) while a ` +
698
- `prompt is executing this is not a backgrounded or frozen tab. Retry after queue ` +
699
- `(action:"list") shows running: 0.`);
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 with at least 1 GiB of VRAM is still PINNED when less than 20% is
2221
- * free after /free. CUDA context leftover on an unloaded GPU is a few hundred
2222
- * MB to a couple of GiB, not 80%+ of a 21 GiB card. The reporter's Raylight
2223
- * MiniMax H3 case was device 2 at ~0.8% free (~179 MiB of 21 GiB) next to
2224
- * siblings at ~44% free that occupancy is what this threshold names.
2225
- *
2226
- * Unknown/unreadable counters are NOT pinned: an unknown answer claims
2227
- * nothing in either direction (#1473). */
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.vram_total;
2232
- const free = d.vram_free;
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 = (getComfyUIBaseUrl() || "").replace(/\/+$/, "");
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 freezes the tab's graph_* channel. Refuse
7155
- // BEFORE dispatch so a mutation is known-not-applied rather than
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
- }, async (args, ctx) => ctx.call({
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