comfyui-mcp 0.50.63 → 0.50.65

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.
@@ -985,6 +985,36 @@ async function probeDeclineRecovery(base, windowMs, intervalMs, probeTimeoutMs,
985
985
  * Origin carries NO path, a boot target mounted under a basePath fails path-aware
986
986
  * identity and is (soundly) fail-closed to dispatched-unconfirmed.
987
987
  */
988
+ /**
989
+ * #851 — what to tell the caller about `restart_comfyui` when the panel's confirmation
990
+ * card timed out.
991
+ *
992
+ * The old text recommended it unconditionally. It targets COMFYUI_URL, not the ComfyUI
993
+ * the panel runs inside, so a reporter who followed that advice aimed a restart at a
994
+ * different server than the one they had been working on — and got "No ComfyUI process
995
+ * found on port 8188" while the panel was working fine on the live canvas.
996
+ *
997
+ * `panelBase` comes from captureRebootHealthBase(), which is a PROOF rather than a
998
+ * comparison: null in remote/cloud mode, and whenever the tab's origin cannot be shown
999
+ * to front the local boot instance. "Unproven" is deliberately NOT folded into
1000
+ * "different" — that would fire the warning on ordinary remote installs where the two
1001
+ * ARE the same server, trading a wrong recommendation for a wrong alarm.
1002
+ *
1003
+ * Pure, so the three-way decision is testable without a live panel.
1004
+ */
1005
+ export function restartTimeoutFallbackAdvice({ headlessBase, panelBase, }) {
1006
+ if (panelBase != null && sameHttpBase(headlessBase, panelBase)) {
1007
+ return "or use restart_comfyui to restart the server directly without a panel card.";
1008
+ }
1009
+ if (panelBase != null) {
1010
+ return (`Do NOT reach for restart_comfyui here without checking: it targets ${headlessBase}, ` +
1011
+ `while this panel is running inside ${panelBase}. Restarting the first would hit a ` +
1012
+ "different server than the one you have been working on — and may find nothing there at all.");
1013
+ }
1014
+ return (`or use restart_comfyui, which restarts ${headlessBase} directly without a panel ` +
1015
+ "card — check that this is the same ComfyUI you have been working on, since I " +
1016
+ "could not confirm which one this panel is running inside.");
1017
+ }
988
1018
  function captureRebootHealthBase(ctx) {
989
1019
  if (isCloudMode() || isRemoteMode())
990
1020
  return null;
@@ -1402,6 +1432,102 @@ async function confirmEmptyOutline(ctx, res, reread) {
1402
1432
  * a longer poll would buy rarer restores at the expense of every empty read.
1403
1433
  */
1404
1434
  const EMPTY_OUTLINE_RECHECK_STEPS_MS = [250, 400, 550];
1435
+ /**
1436
+ * Enforce `max_chars` when the PANEL could not (#1203).
1437
+ *
1438
+ * A panel older than the budget protocol ignores `max_chars` and returns the
1439
+ * whole outline. The orchestrator already noticed — that is what
1440
+ * `markBudgetIgnored` is for — and then forwarded the unbounded reply anyway,
1441
+ * with a note explaining that the bound the caller had explicitly set did not
1442
+ * apply. A 137-node outline arrived that way, which is the exact context flood
1443
+ * `max_chars` exists to prevent, and the disclosure came AFTER the cost was
1444
+ * already paid. A warning is not a bound.
1445
+ *
1446
+ * WHY REFUSE RATHER THAN TRUNCATE. This tool's contract is that coverage is
1447
+ * never traded away: over budget the outline sheds RESOLUTION — widget values,
1448
+ * then titles, then a per-group summary — so whatever comes back still describes
1449
+ * the whole graph, and if even the floor will not fit it returns NO outline
1450
+ * rather than a partial one that reads as complete. Cutting the rendered text at
1451
+ * a character count would break exactly that guarantee: the result would look
1452
+ * like a finished outline that simply ends, and a caller acting on it would
1453
+ * believe the graph stops where the string does — the #1184 failure again, with
1454
+ * the truncation invisible instead of announced.
1455
+ *
1456
+ * Shedding resolution is the panel's job and needs the graph, not its rendering.
1457
+ * The orchestrator has only the rendered text, so the honest move is the one the
1458
+ * panel itself makes at its floor: return no outline, and say precisely what
1459
+ * happened, how big the outline actually was, and which of the three levers
1460
+ * (raise the bound, update the panel, scope the read) will work.
1461
+ *
1462
+ * The counts and `viewing` survive — they are the part of the reply that is
1463
+ * small, true, and independent of the budget.
1464
+ */
1465
+ function enforceOutlineBudget(res, requested) {
1466
+ if (typeof requested !== "number")
1467
+ return res;
1468
+ const payload = parseToolResultJson(res);
1469
+ if (!payload)
1470
+ return res;
1471
+ // A panel that supports the budget echoes it back and has already applied its
1472
+ // own ladder. Nothing to enforce, and second-guessing it would be wrong.
1473
+ if (typeof payload.max_chars === "number")
1474
+ return res;
1475
+ const outline = payload.outline;
1476
+ if (typeof outline !== "string")
1477
+ return res;
1478
+ // Within the bound already. The panel ignored the parameter, but the reply
1479
+ // honours it, and refusing a reply that FITS would be a bound in name only.
1480
+ if (outline.length <= requested)
1481
+ return res;
1482
+ const idx = res.content.findIndex((c) => c.type === "text");
1483
+ if (idx < 0)
1484
+ return res;
1485
+ const nodes = typeof payload.node_count === "number" ? payload.node_count : null;
1486
+ const groups = typeof payload.group_count === "number" ? payload.group_count : null;
1487
+ const shape = nodes != null
1488
+ ? `The graph has ${nodes} node(s)${groups != null ? ` and ${groups} group(s)` : ""}.`
1489
+ : "";
1490
+ // The refusal goes IN the outline field, not an empty string (codex review).
1491
+ // Two reasons. It is what the panel itself does at its own floor, so a caller
1492
+ // that handles one refusal handles both. And an empty outline next to
1493
+ // `node_count: 137` is exactly the contradiction #1184 was about — a consumer
1494
+ // that reads `outline` and finds nothing has been told the canvas is empty by
1495
+ // a reply whose own counts say otherwise. Bounded and fixed-length, so it
1496
+ // cannot itself overrun a budget whose floor is OUTLINE_MAX_CHARS_FLOOR.
1497
+ payload.outline =
1498
+ `NO OUTLINE — this panel build ignores \`max_chars\`, and its full reply (${outline.length} chars) exceeds the bound of ${requested} you set. ` +
1499
+ `It is withheld rather than cut, because a truncated outline would read as a complete one. ` +
1500
+ `${shape} See \`max_chars_hint\` for what to do.`.trim();
1501
+ payload.detail_level = "refused";
1502
+ payload.degraded = true;
1503
+ payload.budget_applied_by = "orchestrator";
1504
+ payload.unbounded_chars = outline.length;
1505
+ payload.degraded_reason =
1506
+ `This panel build ignores \`max_chars\`, so it returned the FULL outline (${outline.length} chars) against a bound of ${requested}. ` +
1507
+ `It is withheld rather than cut: a truncated outline would read as a complete one. ${shape}`.trim();
1508
+ return {
1509
+ ...res,
1510
+ content: res.content.map((c, i) => i === idx && c.type === "text" ? { ...c, text: JSON.stringify(payload, null, 2) } : c),
1511
+ };
1512
+ }
1513
+ /** The three levers when the orchestrator had to withhold an outline (#1203),
1514
+ * in the order worth trying. Shared so the two riders cannot drift. */
1515
+ function outlineBudgetRemedies(unbounded) {
1516
+ // Every "raise it" must name the ceiling, or a caller already at the maximum
1517
+ // is sent on a retry that cannot change anything (the repo-wide remedy check,
1518
+ // which caught this text before it shipped).
1519
+ // The ceiling has to sit next to the parameter name, not at the end of the
1520
+ // sentence: the remedy check reads "raise `X` … up to N" as one phrase, and it
1521
+ // is right to — a caller skimming for whether the retry is possible should not
1522
+ // have to finish the clause.
1523
+ const raise = unbounded == null
1524
+ ? `Raise \`max_chars\` up to ${OUTLINE_MAX_CHARS_CEILING}`
1525
+ : unbounded <= OUTLINE_MAX_CHARS_CEILING
1526
+ ? `Raise \`max_chars\` up to ${OUTLINE_MAX_CHARS_CEILING} — this outline needs at least ${unbounded} to be accepted whole`
1527
+ : `Raising \`max_chars\` will NOT help — the full outline is past its ceiling of ${OUTLINE_MAX_CHARS_CEILING}`;
1528
+ return (`${raise}; update the ComfyUI Agent Panel, which can shed per-node detail and still cover every node inside a smaller bound; ` +
1529
+ `or scope the read with panel_query_graph {ids:[…], fields:'detail'}.`);
1530
+ }
1405
1531
  function markBudgetIgnored(res, requested) {
1406
1532
  if (typeof requested !== "number")
1407
1533
  return res;
@@ -5737,8 +5863,11 @@ export function buildPanelToolDefs() {
5737
5863
  // The synthetic `__budget_ignored` flag below is derived from the reply, not
5738
5864
  // sent by the panel: a build that supports the budget echoes `max_chars` back.
5739
5865
  markBudgetIgnored(
5866
+ // #1203 — and ENFORCE it here when the panel could not, before the
5867
+ // unbounded outline reaches the caller who asked for a bound.
5868
+ enforceOutlineBudget(
5740
5869
  // #1184 — an empty outline is re-verified before it is believed.
5741
- await confirmEmptyOutline(ctx, await ctx.call({ cmd: "graph_outline", max_chars: args.max_chars }), () => ctx.call({ cmd: "graph_outline", max_chars: args.max_chars })), args.max_chars), [
5870
+ await confirmEmptyOutline(ctx, await ctx.call({ cmd: "graph_outline", max_chars: args.max_chars }), () => ctx.call({ cmd: "graph_outline", max_chars: args.max_chars })), args.max_chars), args.max_chars), [
5742
5871
  {
5743
5872
  // #809 (codex gate): a panel older than this budget IGNORES `max_chars` and
5744
5873
  // returns the full outline, so the bound this tool advertises silently did
@@ -5747,8 +5876,24 @@ export function buildPanelToolDefs() {
5747
5876
  // unbounded reply as "this fitted".
5748
5877
  flag: "__budget_ignored",
5749
5878
  key: "max_chars_hint",
5750
- text: (p) => `This panel build does not support \`max_chars\` on the outline, so the budget you set (${typeof args.max_chars === "number" ? args.max_chars : OUTLINE_MAX_CHARS_DEFAULT}) was NOT applied and the outline below is the full, unbounded one (${typeof p.node_count === "number" ? p.node_count : "all"} node(s)). ` +
5751
- `Update the ComfyUI Agent Panel to bound it, or scope the read with panel_query_graph in the meantime.`,
5879
+ text: (p) => {
5880
+ const asked = typeof args.max_chars === "number" ? args.max_chars : OUTLINE_MAX_CHARS_DEFAULT;
5881
+ const nodes = typeof p.node_count === "number" ? p.node_count : "all";
5882
+ // #1203 — three different situations used to share one sentence
5883
+ // that described only the worst of them. Saying "the outline
5884
+ // below is the full, unbounded one" when it was withheld, or
5885
+ // when it fitted anyway, is its own false report.
5886
+ if (p.budget_applied_by === "orchestrator") {
5887
+ const unbounded = typeof p.unbounded_chars === "number" ? p.unbounded_chars : null;
5888
+ return (`This panel build does not support \`max_chars\`, so it returned the FULL outline for all ${nodes} node(s)` +
5889
+ `${unbounded != null ? ` (${unbounded} chars)` : ""} against your bound of ${asked}. ` +
5890
+ `NO outline is included: it is withheld rather than cut, because a truncated outline would read as a complete one. ` +
5891
+ outlineBudgetRemedies(unbounded));
5892
+ }
5893
+ return (`This panel build does not support \`max_chars\`, so the budget you set (${asked}) was not applied by the panel — ` +
5894
+ `the outline below is its full, unbounded reply for all ${nodes} node(s), which happens to fit within your bound. ` +
5895
+ `Update the ComfyUI Agent Panel to have the budget applied properly; on a larger graph an unbounded reply will not fit.`);
5896
+ },
5752
5897
  },
5753
5898
  {
5754
5899
  // On an older panel this is never set either, so the rider is inert there.
@@ -5767,6 +5912,21 @@ export function buildPanelToolDefs() {
5767
5912
  // it "still covers ALL nodes" would describe content the reader cannot
5768
5913
  // see, which is the same lie as a silent cut.
5769
5914
  if (p.detail_level === "refused") {
5915
+ // #1203 — an ORCHESTRATOR refusal is a different situation and
5916
+ // needs different advice. The panel refuses because even its
5917
+ // smallest whole-graph form did not fit, so the question is
5918
+ // whether raising the bound can ever help. Here the panel never
5919
+ // applied the bound at all: the full outline's size is known
5920
+ // exactly, updating the panel unlocks the shed ladder, and the
5921
+ // generic text's "this build does not report how large the
5922
+ // smallest form would be" would send the reader chasing a
5923
+ // capability the reply is already explaining is absent.
5924
+ if (p.budget_applied_by === "orchestrator") {
5925
+ const unbounded = typeof p.unbounded_chars === "number" ? p.unbounded_chars : null;
5926
+ return (`NO outline was returned: this panel build ignores \`max_chars\`, so its reply${unbounded != null ? ` (${unbounded} chars)` : ""} exceeded \`max_chars\`=${inForce} and a PARTIAL outline is deliberately withheld because it would read as complete. ` +
5927
+ `The graph has ${nodes} node(s) and ${groups} group(s). ` +
5928
+ outlineBudgetRemedies(unbounded));
5929
+ }
5770
5930
  // And if the floor exceeds the CEILING, raising is a guaranteed second
5771
5931
  // refusal — a dead retry inside the message explaining the first.
5772
5932
  //
@@ -7925,11 +8085,26 @@ export function buildPanelToolDefs() {
7925
8085
  const confirmBudget = Math.max(1, Math.min(RESTART_CONFIRM_TIMEOUT_MS, overallDeadline - Date.now()));
7926
8086
  const decision = await ctx.confirm("Restart ComfyUI now? It (and this agent) will go down briefly, then reconnect and resume automatically.", "Restart ComfyUI", confirmBudget);
7927
8087
  if (decision === "timeout") {
8088
+ // #851 — the fallback used to be recommended unconditionally, and
8089
+ // `restart_comfyui` targets COMFYUI_URL, NOT the ComfyUI the panel is
8090
+ // running inside. A reporter followed this advice against a panel driving a
8091
+ // different server and got `No ComfyUI process found on port 8188` while the
8092
+ // panel was working fine on the live canvas — a restart aimed at the wrong
8093
+ // machine, recommended by a call that never checked which machine that was.
8094
+ //
8095
+ // The information was already here and simply not consulted on this branch:
8096
+ // captureRebootHealthBase() is the origin the PANEL answers on, and the
8097
+ // adjacent decline branch already reads it. Only recommend the headless tool
8098
+ // when it provably points at the same server; otherwise name both and say
8099
+ // plainly that this call did not verify the other one.
8100
+ const fallback = restartTimeoutFallbackAdvice({
8101
+ headlessBase: getComfyUIBaseUrl(),
8102
+ panelBase: captureRebootHealthBase(ctx),
8103
+ });
7928
8104
  return ok(`No confirmation received within ${Math.round(confirmBudget / 1000)}s, so I did NOT ` +
7929
8105
  "restart ComfyUI. The panel tab may be backgrounded or still reconnecting after a " +
7930
8106
  "previous restart, so the confirmation card wasn't answered. Tell me to restart it " +
7931
- "and I'll re-ask, or use restart_comfyui to restart the server directly without a " +
7932
- "panel card.");
8107
+ `and I'll re-ask. ${fallback}`);
7933
8108
  }
7934
8109
  if (decision !== "yes") {
7935
8110
  // #742: NEVER claim "not restarted" while the server is actually DOWN —