comfyui-mcp 0.48.26 → 0.48.27

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.
@@ -244,6 +244,57 @@ function isRetrySafeCmd(cmd) {
244
244
  const name = typeof cmd.cmd === "string" ? cmd.cmd : "";
245
245
  return RETRY_SAFE_CMDS.has(name);
246
246
  }
247
+ // Graph-EDIT mutations that CHANGE the user's canvas (undoable edits). These are
248
+ // the #436 bug surface: a real side effect the bridge will NOT auto-retry, so —
249
+ // unlike a read — such a command can be neither parked mid-command nor retried
250
+ // once, and firing it into the post-restart "Connected: none" window fails with
251
+ // "no connected tab". It must await a stable binding BEFORE dispatch.
252
+ //
253
+ // This is an EXPLICIT ALLOWLIST, deliberately NOT "everything not read-only":
254
+ // several genuine reads/probes/views that flow through ctx.call are absent from
255
+ // BRIDGE_READONLY_CMDS (e.g. graph_list_subgraphs, training_get_state,
256
+ // graph_canvas, graph_screenshot), so an exclusion rule would wrongly make THOSE
257
+ // wait out the reconnect budget. Under-inclusion here is at worst an unfixed edge
258
+ // (a command keeps today's behavior); over-inclusion would regress a read — so we
259
+ // list only commands that unambiguously mutate the graph. Keep in sync when new
260
+ // graph-edit tools are added (mirrors the RETRY_SAFE_CMDS maintenance model).
261
+ const MUTATING_GRAPH_EDIT_CMDS = new Set([
262
+ "graph_add_node",
263
+ "graph_remove_node",
264
+ "graph_clear",
265
+ "graph_connect",
266
+ "graph_disconnect",
267
+ "graph_set_widget",
268
+ "graph_move_node",
269
+ "graph_resize_node",
270
+ "graph_set_title",
271
+ "graph_set_node_mode",
272
+ "graph_set_node_color",
273
+ "graph_set_node_collapsed",
274
+ "graph_update_node",
275
+ "graph_create_group",
276
+ "graph_edit_group",
277
+ "graph_remove_group",
278
+ "graph_move_group",
279
+ "graph_create_subgraph",
280
+ "graph_add_subgraph",
281
+ "graph_save_subgraph",
282
+ "graph_unpack_subgraph",
283
+ "graph_subgraph_group",
284
+ "graph_expose_subgraph_input",
285
+ "graph_expose_subgraph_output",
286
+ "graph_promote_widget",
287
+ "graph_move_rail",
288
+ "graph_paste_nodes",
289
+ "graph_auto_layout",
290
+ "graph_load",
291
+ ]);
292
+ /** A MUTATING graph edit that must await a stable tab binding before dispatch so
293
+ * it never fires into the post-restart "Connected: none" window (#436). */
294
+ function isMutatingGraphCmd(cmd) {
295
+ const name = typeof cmd.cmd === "string" ? cmd.cmd : "";
296
+ return MUTATING_GRAPH_EDIT_CMDS.has(name);
297
+ }
247
298
  /** True when an error is a TRANSIENT transport/reconnect drop (the tab went away
248
299
  * or was replaced), NOT a genuine command error or a live-but-frozen reply
249
300
  * timeout. Deliberately EXCLUDES "did not reply within N ms" (a backgrounded/
@@ -1525,6 +1576,21 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
1525
1576
  };
1526
1577
  const call = async (cmd, timeoutMs) => {
1527
1578
  try {
1579
+ // #436: a MUTATING graph edit must not fire into the "Connected: none"
1580
+ // window a ComfyUI restart/reload opens. A read survives that window (it is
1581
+ // parked mid-command and is retry-safe), but a mutating edit is NEITHER — so
1582
+ // panel_graph_outline succeeds while the very next panel_add_node hits
1583
+ // resolveTarget's momentarily-empty registry and fails with
1584
+ // "no connected tab … Connected: none" (the flap). Await a stable binding
1585
+ // BEFORE dispatch — nothing is sent during the wait, so there is no
1586
+ // double-apply risk — exactly as workflow_open/save already do. This returns
1587
+ // INSTANTLY for a healthy session and only waits in the zero-tab window; the
1588
+ // read path (parking + retry-once below) is left exactly as-is. A wait that
1589
+ // times out unreached still falls through to sendRouted, whose authoritative
1590
+ // dispatched:false surfaces the actionable "nothing applied — rebind" message.
1591
+ if (isMutatingGraphCmd(cmd)) {
1592
+ await awaitReachable();
1593
+ }
1528
1594
  ensureReachable();
1529
1595
  return ok(await sendRouted(cmd, timeoutMs));
1530
1596
  }
@@ -1552,6 +1618,33 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
1552
1618
  return fail(err2);
1553
1619
  }
1554
1620
  }
1621
+ // #442 defect 4: a MUTATING command (deliberately excluded from RETRY_SAFE_CMDS)
1622
+ // that the bridge refused BEFORE any socket write surfaced the bare routing error
1623
+ // ("no connected tab … Connected: none") with no recovery path — whereas a
1624
+ // retry-safe read like graph_get_errors, via the branch above, names the rebind.
1625
+ // That asymmetry made a brief post-reconnect read/edit-channel disagreement look
1626
+ // like a dead agent (panel_list_workflows kept answering while panel_set_widget
1627
+ // failed, in a multi-tab session the strict-single silent auto-heal won't touch).
1628
+ // We must NOT retry the mutating command (double-apply risk), but the bridge's
1629
+ // AUTHORITATIVE typed flag proves nothing was dispatched (dispatchOutcomeOf ===
1630
+ // false) — so it is safe to state nothing was applied and name the rebind recovery,
1631
+ // preserving the raw cause. Keying on the TYPED flag (not error text) means a
1632
+ // POST-dispatch executor ok:false reply that merely quotes "no connected tab" is
1633
+ // never mis-wrapped as "nothing applied".
1634
+ if (dispatchOutcomeOf(err) === false) {
1635
+ const name = typeof cmd.cmd === "string" ? cmd.cmd : "panel command";
1636
+ // Neutral wording: a dispatched:false flag proves only that the command was NOT
1637
+ // dispatched — it can be a routing refusal (the bound tab is gone / reconnecting),
1638
+ // an ambiguous-or-multiple-tab resolver refusal (other tabs DO exist), or a socket
1639
+ // write failure. All share the same TRUE facts (nothing applied) and the same
1640
+ // recovery (rebind onto the tab that's live now); the raw cause carries the
1641
+ // specifics. Do NOT overstate "disconnected", which is false for the ambiguity case.
1642
+ return fail(`${name} could not be dispatched to this session's panel tab — nothing was applied. ` +
1643
+ `The tab may be disconnected, still reconnecting after a restart/reload, or the ` +
1644
+ `session's binding is stale (e.g. another workflow tab is now active). Retry in a ` +
1645
+ `moment, or rebind with panel_set_workflow_target({mode:"current"}) to follow the ` +
1646
+ `tab that's live now. (${err instanceof Error ? err.message : String(err)})`);
1647
+ }
1555
1648
  return fail(err);
1556
1649
  }
1557
1650
  };