comfyui-mcp 0.51.32 → 0.51.34

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.
@@ -1739,6 +1739,165 @@ async function settleExitSubgraphAfterAckTimeout(ctx, timedOut) {
1739
1739
  }
1740
1740
  return timedOut;
1741
1741
  }
1742
+ // ---- panel_install_node: accepted-but-never-enqueued (#1129) ---------------
1743
+ // #1143 fixed the pre-queue REFUSAL (403/404 → direct clone). This is the other
1744
+ // half of the same family: legacy Manager 3.x answers the install POST with
1745
+ // `queued: true`, and the task never enters the queue at all. The reporter's
1746
+ // follow-up read showed an idle queue with `total_count: 0` and no directory
1747
+ // under custom_nodes, while this tool had already said "queued".
1748
+ //
1749
+ // It is the third time this repo has met the same lesson, so it is worth stating
1750
+ // once: ComfyUI-Manager's acknowledgement is not a receipt. `getlist` cannot
1751
+ // distinguish an unreachable registry from an empty one; a download reports done
1752
+ // when the QUEUE DRAINS rather than when the transfer finishes; and here an
1753
+ // accepted request is simply dropped. "Accepted" is never evidence of "happened",
1754
+ // so the only honest reply is one that went and looked.
1755
+ //
1756
+ // NOTE the asymmetry with install_custom_node, which already verifies and clones:
1757
+ // that path owns the filesystem it installs into. This one drives whatever
1758
+ // ComfyUI the PANEL is bound to, which need not be this machine — so it can
1759
+ // report the truth but must not quietly clone somewhere else.
1760
+ /** True when the panel's reply claims the install was accepted/queued. */
1761
+ function claimsQueued(reply) {
1762
+ if (!reply)
1763
+ return false;
1764
+ return reply.queued === true || reply.pending === true;
1765
+ }
1766
+ /**
1767
+ * `total_count` counts EVERY task the Manager has seen, completed ones included —
1768
+ * so a fast install that already finished still leaves it ≥ 1. Zero therefore
1769
+ * means nothing was ever enqueued, which is the decisive reading and the
1770
+ * reporter's exact signature.
1771
+ *
1772
+ * Not my arithmetic: `countsFromStatus` in node-management.ts states the 3.x
1773
+ * contract outright — "total_count = done + in_progress + queued exactly" — and
1774
+ * derives pending from it. So `done_count` cannot be non-zero while total is 0.
1775
+ *
1776
+ * v4 reports `pending_count` directly and need not carry `total_count` at all, so
1777
+ * this simply never fires there. That is correct: the dropped-enqueue defect is a
1778
+ * legacy-3.x behaviour, and a shape that cannot answer must not be made to.
1779
+ *
1780
+ * ONE KNOWN WAY THIS READS ZERO AFTER A REAL INSTALL: the counters are cleared by
1781
+ * `POST /manager/queue/reset`, which this codebase itself issues from
1782
+ * manager-config.ts and workflow-deps.ts. Neither is in panel_install_node's
1783
+ * path, and the read below happens immediately after the install returns, so it
1784
+ * takes a CONCURRENT reset from another operation to land in that window.
1785
+ *
1786
+ * That residual case is the reason this only WARNS. A definite failure verdict
1787
+ * would be wrong there, and wrong in the same direction as the bug being fixed —
1788
+ * a confident claim the evidence does not support. A spurious "go and check"
1789
+ * costs one read; a spurious "it definitely failed" costs a reinstall.
1790
+ */
1791
+ function queueNeverSawATask(reply) {
1792
+ const status = (reply?.status ?? reply);
1793
+ if (typeof status?.total_count !== "number" || status.total_count !== 0)
1794
+ return false;
1795
+ // PRESENT and zero, not absent-or-zero (codex P2). `panel-installer.ts`'s
1796
+ // established legacy-empty proof requires every count to be reported and exact;
1797
+ // accepting a missing field would let a payload that never described the queue
1798
+ // stand in for one that did. A `pending_count`, if this build reports one, has
1799
+ // to agree as well.
1800
+ const presentZero = (v) => v === 0;
1801
+ if (!presentZero(status.done_count) || !presentZero(status.in_progress_count))
1802
+ return false;
1803
+ if (status.pending_count !== undefined && !presentZero(status.pending_count))
1804
+ return false;
1805
+ // IDLE TOO, not just empty (codex P1). A snapshot taken while the Manager is
1806
+ // mid-accept can read zero before its counters move, and a running worker is
1807
+ // the one state where zero means "not yet" rather than "never". The panel's own
1808
+ // queue predicate draws the same line, so this matches rather than invents one.
1809
+ // Absent/non-boolean is NOT treated as idle: unknown answers nothing.
1810
+ if (status.is_processing !== false)
1811
+ return false;
1812
+ // Coherent, by the 3.x contract quoted above: with total 0, both of these must
1813
+ // be 0 as well. Anything else is a shape this reasoning does not describe.
1814
+ return true;
1815
+ }
1816
+ /**
1817
+ * After an install the panel reported as queued, confirm it actually was.
1818
+ *
1819
+ * Two NEGATIVE observations are required before this contradicts the panel: the
1820
+ * queue never saw a task, AND the pack is absent from the installed list. Either
1821
+ * alone is too weak — a queue shape without `total_count` proves nothing, and a
1822
+ * pack missing from the list moments after enqueuing is normal, because it has
1823
+ * not been cloned yet. Together they are the reported failure exactly.
1824
+ *
1825
+ * Anything inconclusive returns the panel's own reply untouched (#1473's rule).
1826
+ */
1827
+ /**
1828
+ * The panel identity a route key currently resolves to, or `undefined` when this
1829
+ * bridge cannot report one.
1830
+ *
1831
+ * `undefined` means UNKNOWN, and callers must treat it as such rather than as
1832
+ * "unchanged" (codex, final pass). Comparing two unknowns yields equality, which
1833
+ * would let a same-key takeover pass the guard while the message claims the read
1834
+ * happened "on that same panel" — a false statement produced by a guard that
1835
+ * cannot see. The real UiBridge always implements this; only lightweight or mock
1836
+ * contexts do not, and those simply do not get the warning.
1837
+ */
1838
+ function panelIncarnation(ctx, tabId) {
1839
+ const b = ctx.bridge;
1840
+ return typeof b.tabIncarnation === "function" ? b.tabIncarnation(tabId) : undefined;
1841
+ }
1842
+ async function settleDroppedEnqueue(ctx, res, dispatch) {
1843
+ if (res.isError)
1844
+ return res;
1845
+ if (!claimsQueued(parseToolResultJson(res)))
1846
+ return res;
1847
+ // The queue is only evidence about the panel the install was DISPATCHED to
1848
+ // (codex P1 — and the same guard #1468 needed, which I did not carry across).
1849
+ // `ctx.call` runs ensureReachable first, which silently rebinds an unpinned
1850
+ // current-mode session onto the sole remaining interactive tab. A reconnect
1851
+ // between the two calls would let ANOTHER ComfyUI's empty queue be reported as
1852
+ // evidence about this install.
1853
+ // The ROUTE KEY alone is not the panel (codex P1, round 3). A `wf:` key is
1854
+ // `wf:<tabRouteId>:<path>` — it names a WORKFLOW, so it recurs, and a different
1855
+ // browser tab can take it over without `ctx.tabId` changing at all. The bridge
1856
+ // draws exactly this distinction and exposes `tabIncarnation` for it (#486), so
1857
+ // both are captured: the key AND the incarnation currently holding it.
1858
+ const queue = await ctx.call({ cmd: "nodes_queue_status" }, 15000);
1859
+ if (ctx.tabId !== dispatch.tab)
1860
+ return res;
1861
+ // Both captured BEFORE the install was dispatched, not here — a takeover that
1862
+ // happens DURING the install is already baked in by the time this function
1863
+ // runs, so comparing two post-install readings would always agree and the guard
1864
+ // would be decorative. Its own test caught exactly that.
1865
+ // UNKNOWN is not "unchanged": a bridge that cannot report an incarnation cannot
1866
+ // rule out a same-key takeover, so it does not get to make a claim about which
1867
+ // panel answered.
1868
+ if (dispatch.incarnation === undefined)
1869
+ return res;
1870
+ if (panelIncarnation(ctx, ctx.tabId) !== dispatch.incarnation)
1871
+ return res;
1872
+ if (queue.isError || !queueNeverSawATask(parseToolResultJson(queue)))
1873
+ return res;
1874
+ // NO PROVENANCE CLAIM AT ALL — deliberately, after five review rounds.
1875
+ //
1876
+ // Earlier drafts explained WHY the task was probably dropped ("the Manager
1877
+ // accepted a git URL it does not recognise") and prescribed accordingly. Every
1878
+ // round found that explanation false in some reachable state: an id-only
1879
+ // install submits no URL; a `repository` that is any non-empty string is not
1880
+ // necessarily a URL. Each fix branched on a fact the request does not reliably
1881
+ // carry, and each branch was a fresh chance to assert the wrong cause.
1882
+ //
1883
+ // What this function actually observed is the queue reading. That is worth
1884
+ // saying, needs no provenance, and cannot be wrong. The cause belongs to
1885
+ // whoever can see the install — so the message asks for the ONE check that
1886
+ // settles it and stops there. Less useful in the common case; never false.
1887
+ return appendNote(res, `WARNING — THE QUEUE DOES NOT HAVE THIS TASK. The Manager accepted the install above, but a ` +
1888
+ `read taken immediately afterwards, on that same panel, reports an IDLE queue holding no ` +
1889
+ `tasks at all (total_count, done, in_progress all 0). "queued" is its acknowledgement, not ` +
1890
+ `a receipt.\n\n` +
1891
+ `THIS IS NOT PROOF EITHER WAY. Those counters are also cleared by a queue RESET, which ` +
1892
+ `other operations in this server issue, so an install that really ran can read exactly ` +
1893
+ `like this if a reset landed in between.\n\n` +
1894
+ `SO CHECK: call panel_list_nodes and see whether the pack is actually there. Do not restart ` +
1895
+ `on the assumption it installed, and do not reinstall on the assumption it did not.\n\n` +
1896
+ `IF IT IS ABSENT: install it through the headless install_custom_node, which verifies the ` +
1897
+ `pack really landed instead of trusting the queue, and can clone a repository URL directly ` +
1898
+ `when the Manager will not take it. This tool cannot clone for you — it drives whatever ` +
1899
+ `ComfyUI the panel is bound to, which need not be this machine.`);
1900
+ }
1742
1901
  /** Parse a ctx.call ToolResult's text payload as JSON, or null if not parseable. */
1743
1902
  function parseToolResultJson(res) {
1744
1903
  if (!res || res.isError)
@@ -9732,14 +9891,28 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
9732
9891
  const { conflict, note, ...cmdArgs } = nodesInstallCommandArgs(args);
9733
9892
  if (conflict)
9734
9893
  return fail(conflict);
9894
+ // #1129 — the panel identity is captured BEFORE dispatch, because a
9895
+ // takeover during the install is exactly what the follow-up read must not
9896
+ // be attributed to.
9897
+ const dispatch = {
9898
+ tab: ctx.tabId,
9899
+ incarnation: panelIncarnation(ctx, ctx.tabId),
9900
+ };
9735
9901
  const res = await ctx.call({ cmd: "nodes_install", ...cmdArgs }, 30000);
9902
+ // #1129 — settle BEFORE the note is appended. The note is glued on after
9903
+ // the JSON body, which makes the payload unparseable as JSON, so a probe
9904
+ // running afterwards reads `null`, concludes the panel never claimed a
9905
+ // queue, and silently does nothing. Found by printing the real reply
9906
+ // rather than by reasoning about it: the first version of this shipped
9907
+ // the check and the check never ran.
9908
+ const settled = await settleDroppedEnqueue(ctx, res, dispatch);
9736
9909
  if (note) {
9737
- const text = res.content.find((c) => c.type === "text");
9910
+ const text = settled.content.find((c) => c.type === "text");
9738
9911
  if (text && text.type === "text") {
9739
9912
  text.text += `\n\nNOTE: ${note}`;
9740
9913
  }
9741
9914
  }
9742
- return res;
9915
+ return settled;
9743
9916
  }),
9744
9917
  def("panel_update_node", "Update an ALREADY-INSTALLED custom-node pack to its latest (or nightly) code via the BUILT-IN Manager — the first thing to try when a node is broken or CRASHED ComfyUI (e.g. from a crash dump injected on resume). Pass `id` = the installed pack's name/dir (e.g. 'ComfyUI-WanVideoWrapper' from the crash culprit, or an id from panel_list_nodes). Use version 'nightly' to pull the very latest commit (good when a fix just landed upstream), else 'latest' for the newest release. Queues the update; poll panel_node_queue_status, then panel_restart_comfyui to load it. If updating doesn't fix the crash, escalate (git pull / source patch) per your steering.", {
9745
9918
  id: z.string().describe("Installed pack name or dir (e.g. 'ComfyUI-WanVideoWrapper'), or a registry id from panel_list_nodes."),