comfyui-mcp 0.50.100 → 0.50.102

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.
@@ -1341,6 +1341,28 @@ function toolResultText(res) {
1341
1341
  * else about it — including `isError` and any non-text content. Used where the
1342
1342
  * original message must reach the caller VERBATIM and we are only adding what we
1343
1343
  * additionally know, never restating (or re-classifying) what it already said. */
1344
+ /** All text in a tool result, joined — for quoting one result inside another. */
1345
+ function textOfToolResult(res) {
1346
+ return res.content.map((c) => c.text ?? "").join(" ").trim();
1347
+ }
1348
+ /**
1349
+ * #1329 — the panel's STALE NODE SCHEMA refusal, which is worth acting on rather than
1350
+ * relaying.
1351
+ *
1352
+ * It is the one refusal here whose prescribed remedy has no decision in it: the class
1353
+ * this page registered has drifted from the server's, and `refresh_nodes` re-fetches
1354
+ * /object_info and re-registers it in place. The reporter ran that by hand twice and it
1355
+ * worked twice.
1356
+ *
1357
+ * Matched on the drift sentence rather than on "panel_refresh_nodes", which appears in
1358
+ * several unrelated remedies — keying on the recommendation would make any of them
1359
+ * trigger an add-retry.
1360
+ */
1361
+ function isStaleNodeSchemaRefusal(res) {
1362
+ if (!res.isError)
1363
+ return false;
1364
+ return /added or retyped since this page loaded its node schema/i.test(textOfToolResult(res));
1365
+ }
1344
1366
  function appendToolResultText(res, extra) {
1345
1367
  const idx = res?.content?.findIndex((c) => c.type === "text") ?? -1;
1346
1368
  const block = idx >= 0 ? res.content[idx] : undefined;
@@ -2487,6 +2509,67 @@ export function readOpenActiveAgainstTarget(active, path, activeConfirmed) {
2487
2509
  * all. Requiring corroboration there is what kept the reporter's own case
2488
2510
  * unexamined.
2489
2511
  */
2512
+ /**
2513
+ * #1337 — CLEAR THE FENCE ON THE VERDICT THAT PROVED IDENTITY.
2514
+ *
2515
+ * A reporter's session lost the canvas permanently: every graph call refused with
2516
+ * `workflow instance mismatch`, and the one fence-EXEMPT recovery could not clear it.
2517
+ *
2518
+ * `workflow_open` re-derives the fence only on its success branch. The verdict
2519
+ *
2520
+ * "workflow_open RAN and the canvas IS bound to X — that much was proven — but the
2521
+ * graph on it does not match the state that was loaded … You are NOT on the wrong
2522
+ * workflow: X IS the active one."
2523
+ *
2524
+ * is delivered as an ERROR, so control never reaches the re-derivation. That mismatches
2525
+ * two different levels: the verdict asserts IDENTITY was proven, and what it withholds
2526
+ * is CONTENT (frontend normalisation vs a partial load). Applying content-level
2527
+ * uncertainty to an identity-level fence is what leaves the session with no in-band
2528
+ * recovery — the only remaining move is a browser reload, which destroys unsaved work.
2529
+ *
2530
+ * MEASURED, because the comment that justified the old behaviour asserts otherwise:
2531
+ * • the panel really does publish NO workflow_uuid on this reply (its own
2532
+ * FENCE_NOT_REFRESHED text), so there is nothing to adopt FROM THE REPLY — true;
2533
+ * • but `workflow_list` IS fence-exempt (commandIsCanvasTargetless in the panel's
2534
+ * workflow-chat-identity.js), and the panel's own recovery text names
2535
+ * panel_list_workflows as "exempt from the fence" and says it "republishes the
2536
+ * active identity". So the fence CAN be re-derived here; nothing tried.
2537
+ *
2538
+ * This runs that exempt re-derivation and reports what it achieved. The open still
2539
+ * FAILS — the content warning is preserved verbatim, because "re-read before editing"
2540
+ * is still the right instruction — and nothing is adopted on the UNPROVEN verdict,
2541
+ * where identity itself is in doubt.
2542
+ */
2543
+ async function clearFenceOnIdentityProvenOpen(ctx, res) {
2544
+ const text = toolResultText(res);
2545
+ // ONLY the class that states identity was proven. The UNPROVEN verdict ("could not
2546
+ // prove that the active canvas was rebound") must keep failing closed: re-deriving a
2547
+ // fence onto a canvas we cannot identify is how an edit lands on the wrong graph.
2548
+ if (!/the canvas IS bound to/i.test(text))
2549
+ return res;
2550
+ let note;
2551
+ try {
2552
+ const rebind = await rebindWorkflowFence(ctx);
2553
+ note =
2554
+ rebind.status === "refreshed" || rebind.status === "already_current"
2555
+ ? `\n\nFENCE: CLEARED. This reply published no workflow_uuid, so the fence was re-derived ` +
2556
+ `from the panel's own fence-EXEMPT workflow_list read instead — the active identity is ` +
2557
+ `${rebind.uuid}, and graph commands from this session are stamped with it again. You do ` +
2558
+ `NOT need to reload the browser tab, and no unsaved canvas work is at risk. The content ` +
2559
+ `warning above still stands: re-read the graph before editing.`
2560
+ : `\n\nFENCE: NOT cleared (${rebind.status}). The re-derivation was attempted — the panel ` +
2561
+ `publishes no workflow_uuid on this verdict, so the fence-exempt workflow_list read is ` +
2562
+ `the only way to refresh it — and it did not establish an identity. Graph commands stay ` +
2563
+ `refused. Try panel_list_workflows directly; if that also fails, the panel tab is the ` +
2564
+ `thing to fix.`;
2565
+ }
2566
+ catch (err) {
2567
+ note =
2568
+ `\n\nFENCE: NOT cleared — the re-derivation threw, so the fence state is UNKNOWN and graph ` +
2569
+ `commands may still be refused. (${err instanceof Error ? err.message : String(err)})`;
2570
+ }
2571
+ return appendToolResultText(res, note);
2572
+ }
2490
2573
  async function observeActiveAfterOpen(ctx, requestedPath) {
2491
2574
  let list = null;
2492
2575
  try {
@@ -3621,7 +3704,7 @@ async function openWorkflowWithVerify(path, ctx) {
3621
3704
  return res;
3622
3705
  const drift = await observeActiveAfterOpen(ctx, path);
3623
3706
  if (!drift)
3624
- return res;
3707
+ return await clearFenceOnIdentityProvenOpen(ctx, res);
3625
3708
  return fail(`${toolResultText(res)}\n\nAND — checked after that failure — ${drift.activeLabel} is the ` +
3626
3709
  `ACTIVE workflow now, not ${path}. Whatever that message says about ${path} being active, ` +
3627
3710
  `it is not: the canvas you would read or write is ${drift.activeLabel}. Do NOT save, ` +
@@ -6675,12 +6758,46 @@ export function buildPanelToolDefs() {
6675
6758
  .optional()
6676
6759
  .describe("Canvas [x, y] (two numbers). Auto-placed beside existing nodes when omitted."),
6677
6760
  title: z.string().optional().describe("Optional custom node title."),
6678
- }, async (args, ctx) =>
6679
- // #599: the frontend gates the add on a FRESH /object_info (assertAddNode-
6680
- // ResolvableRefreshing) so an uninstalled class can't be added as a
6681
- // placeholder — that fetch can outlast the 6000 ms default on a large
6682
- // install. Give it the bounded refresh ack budget.
6683
- ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS)),
6761
+ }, async (args, ctx) => {
6762
+ // #599: the frontend gates the add on a FRESH /object_info (assertAddNode-
6763
+ // ResolvableRefreshing) so an uninstalled class can't be added as a
6764
+ // placeholder — that fetch can outlast the 6000 ms default on a large
6765
+ // install. Give it the bounded refresh ack budget.
6766
+ const add = () => ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
6767
+ const first = await add();
6768
+ if (!isStaleNodeSchemaRefusal(first))
6769
+ return first;
6770
+ // #1329 — DO THE REFRESH THE REFUSAL ASKS FOR, instead of billing the caller.
6771
+ //
6772
+ // The reporter uploaded two images, and both LoadImage adds were refused with
6773
+ // "call panel_refresh_nodes and retry". Doing exactly that worked, both times.
6774
+ // That is a round trip and an agent-visible error for something with one
6775
+ // correct response and no decision in it.
6776
+ //
6777
+ // Safe to automate for a checkable reason, not a hopeful one: the panel's guard
6778
+ // throws BEFORE it creates anything ("Refuse before creating anything"), so the
6779
+ // retry cannot leave two nodes behind. Mutations are otherwise never re-issued
6780
+ // on our own initiative, and that rule is intact — this is not a transport
6781
+ // failure with an unknown outcome, it is a refusal that states its own.
6782
+ const refreshed = await ctx.call({ cmd: "refresh_nodes" }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
6783
+ if (refreshed.isError) {
6784
+ // The refusal is the better error: it names what is wrong with the SCHEMA and
6785
+ // what clears it. Carry the refresh failure alongside so the caller knows the
6786
+ // automatic attempt happened and why it did not help.
6787
+ return appendToolResultText(first, `\n\n(Tried to clear this automatically: panel_refresh_nodes was dispatched and FAILED, ` +
6788
+ `so the schema is unchanged and retrying the add will refuse again. ` +
6789
+ `${textOfToolResult(refreshed)})`);
6790
+ }
6791
+ const second = await add();
6792
+ if (!isStaleNodeSchemaRefusal(second))
6793
+ return second;
6794
+ // Still stale after a successful refresh: report THAT, because it means the
6795
+ // remedy the refusal prescribes does not fix this instance and a caller
6796
+ // following it by hand would loop.
6797
+ return appendToolResultText(second, `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
6798
+ `and the add still refuses, so repeating panel_refresh_nodes will not clear it. ` +
6799
+ `Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`);
6800
+ }),
6684
6801
  def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_node", node_id: args.node_id })),
6685
6802
  def("panel_clear", "Remove EVERY node from the user's open graph — only for an explicit 'clear/reset the canvas'. Just CALL THIS DIRECTLY when they ask to clear: the tool itself pops a confirm card and only wipes on a yes (don't ask separately first). The wipe is a single Ctrl+Z undo. NEVER use this for a 'new workflow' — that's panel_new_workflow (a new tab, leaves this graph intact).", {}, async (_args, ctx) => {
6686
6803
  const decision = await ctx.confirm("Clear the canvas? This removes every node from the open workflow. (One Ctrl+Z undoes it.)", "Clear canvas");