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 =
|
|
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
|
|
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."),
|