comfyui-mcp 0.52.181 → 0.52.182
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.
- package/dist/orchestrator/panel-tools.js +146 -17
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/extra-paths.js +11 -0
- package/dist/services/extra-paths.js.map +1 -1
- package/dist/services/rail-slot-connect.js +195 -0
- package/dist/services/rail-slot-connect.js.map +1 -0
- package/dist/services/slot-compat.js +62 -6
- package/dist/services/slot-compat.js.map +1 -1
- package/package.json +1 -1
|
@@ -47,6 +47,7 @@ import { isPreExecutorRefusal } from "../services/panel-refusal.js";
|
|
|
47
47
|
import { hostLinksAlreadyReindexed, isLandedUnexpose, laterSlotsAfterUnexpose, laterSlotsFromUnexposePayload, unexposeHostLinkShiftNote, } from "../services/unexpose-host-link-shift.js";
|
|
48
48
|
import { retryConnectAgainstLiveGraph } from "../services/connect-live-graph.js";
|
|
49
49
|
import { verifyPrimitiveForceInputAfterConnect } from "../services/primitive-force-input-connect.js";
|
|
50
|
+
import { retryRailSlotConnect } from "../services/rail-slot-connect.js";
|
|
50
51
|
import { retryWildcardSlotConnect } from "../services/wildcard-slot-connect.js";
|
|
51
52
|
import { retryExposeSubgraphInput } from "../services/expose-ae-wildcard.js";
|
|
52
53
|
import { applyLiveRootViewing, callAndRememberViewing, callWithRememberedSubgraph, clearStaleSubgraphIdentity, noteConfirmedViewingFromToolResult, parseViewingScope, } from "../services/subgraph-viewing-scope.js";
|
|
@@ -5911,6 +5912,35 @@ function forgetStaleSubgraphIdentity(ctx) {
|
|
|
5911
5912
|
clearStaleSubgraphIdentity(ctx.tabId);
|
|
5912
5913
|
ctx.bridge?.clearPromotedSubgraphIdentity?.(ctx.tabId);
|
|
5913
5914
|
}
|
|
5915
|
+
/** Tabs whose promoted-container mapping is unverified after a queue-busy
|
|
5916
|
+
* mutation refusal. `graph_get_subgraph` can stay indeterminate for ordinary
|
|
5917
|
+
* root nodes until something walks the live graph (#2730). */
|
|
5918
|
+
const staleSubgraphMappingTabs = new Set();
|
|
5919
|
+
function noteStaleSubgraphMapping(tabId) {
|
|
5920
|
+
if (tabId)
|
|
5921
|
+
staleSubgraphMappingTabs.add(tabId);
|
|
5922
|
+
}
|
|
5923
|
+
export function __resetStaleSubgraphMappingForTest() {
|
|
5924
|
+
staleSubgraphMappingTabs.clear();
|
|
5925
|
+
}
|
|
5926
|
+
/** Root graph identity from a live query even when `is_subgraph` is missing.
|
|
5927
|
+
* A missing container bit is indeterminate, but a published root identity is
|
|
5928
|
+
* still enough to spend one mapping refresh before failing closed (#2730). */
|
|
5929
|
+
function parseRootGraphIdentity(payload) {
|
|
5930
|
+
if (!payload)
|
|
5931
|
+
return null;
|
|
5932
|
+
const viewing = payload.viewing;
|
|
5933
|
+
if (!viewing || typeof viewing !== "object" || Array.isArray(viewing))
|
|
5934
|
+
return null;
|
|
5935
|
+
const rec = viewing;
|
|
5936
|
+
if (rec.scope !== "root")
|
|
5937
|
+
return null;
|
|
5938
|
+
const graphIdentity = rec.graph_identity;
|
|
5939
|
+
if (typeof graphIdentity !== "string" || graphIdentity.length === 0 || graphIdentity.length > 256) {
|
|
5940
|
+
return null;
|
|
5941
|
+
}
|
|
5942
|
+
return graphIdentity;
|
|
5943
|
+
}
|
|
5914
5944
|
/** Read the active viewing scope and node's explicit container bit before
|
|
5915
5945
|
* attempting promoted-widget resolution. The pinpoint detail projection carries
|
|
5916
5946
|
* the node bit even when the widget list is empty (which is how a fresh rgthree
|
|
@@ -6644,12 +6674,15 @@ async function readPromotedTargetScope(ctx, nodeId, options = PROMOTED_PREFLIGHT
|
|
|
6644
6674
|
limit: 1,
|
|
6645
6675
|
}, undefined, undefined, undefined, options);
|
|
6646
6676
|
if (probe.isError)
|
|
6647
|
-
return null;
|
|
6677
|
+
return { scope: null, rootGraphIdentity: null };
|
|
6648
6678
|
const payload = parseToolResultJson(probe);
|
|
6649
6679
|
if (parseViewingScope(payload?.viewing)?.scope === "root") {
|
|
6650
6680
|
rememberLiveRootViewing(ctx, payload?.viewing);
|
|
6651
6681
|
}
|
|
6652
|
-
return
|
|
6682
|
+
return {
|
|
6683
|
+
scope: parseVerifiedQueriedNodeScope(payload, nodeId),
|
|
6684
|
+
rootGraphIdentity: parseRootGraphIdentity(payload),
|
|
6685
|
+
};
|
|
6653
6686
|
}
|
|
6654
6687
|
function currentPromotedBindingError(ctx, binding) {
|
|
6655
6688
|
if (ctx.tabId !== binding.tabId)
|
|
@@ -7175,12 +7208,43 @@ async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
|
|
|
7175
7208
|
return promotedWriteRefusal(widget, "the panel connection identity could not be read");
|
|
7176
7209
|
}
|
|
7177
7210
|
}
|
|
7211
|
+
// #2730 — after a queue-busy mutation refusal the panel's subgraph registry
|
|
7212
|
+
// can stay stale until a graph walk. Refresh once before classifying, then
|
|
7213
|
+
// re-probe so an ordinary root node can take the fast path without a manual
|
|
7214
|
+
// panel_graph_outline.
|
|
7215
|
+
let mappingRefreshed = false;
|
|
7216
|
+
const refreshSubgraphMapping = async () => {
|
|
7217
|
+
if (mappingRefreshed)
|
|
7218
|
+
return null;
|
|
7219
|
+
mappingRefreshed = true;
|
|
7220
|
+
const outline = await ctx.call({ cmd: "graph_outline" }, undefined, undefined, undefined, PROMOTED_PREFLIGHT_READ_OPTIONS);
|
|
7221
|
+
if (outline.isError) {
|
|
7222
|
+
return promotedWriteRefusal(widget, "graph_outline could not refresh the subgraph mapping");
|
|
7223
|
+
}
|
|
7224
|
+
staleSubgraphMappingTabs.delete(ctx.tabId);
|
|
7225
|
+
const payload = parseToolResultJson(outline);
|
|
7226
|
+
if (parseViewingScope(payload?.viewing)?.scope === "root") {
|
|
7227
|
+
rememberLiveRootViewing(ctx, payload?.viewing);
|
|
7228
|
+
forgetStaleSubgraphIdentity(ctx);
|
|
7229
|
+
}
|
|
7230
|
+
const drift = panelBindingDriftReason(ctx, "after the subgraph mapping refresh", hasIdentityApi, identityBefore, tabBefore);
|
|
7231
|
+
if (drift)
|
|
7232
|
+
return ordinaryBindingRefusal(widget, drift);
|
|
7233
|
+
return null;
|
|
7234
|
+
};
|
|
7235
|
+
if (staleSubgraphMappingTabs.has(ctx.tabId)) {
|
|
7236
|
+
const refreshError = await refreshSubgraphMapping();
|
|
7237
|
+
if (refreshError)
|
|
7238
|
+
return refreshError;
|
|
7239
|
+
}
|
|
7178
7240
|
// #2394 — a root-level rgthree Power Lora Loader may have no lora_N widget
|
|
7179
7241
|
// yet. Prove the addressed node is an ordinary root node before asking the
|
|
7180
7242
|
// promoted-container classifier to resolve a target that is intentionally
|
|
7181
7243
|
// absent. An unreadable scope probe is not permission for an outer write;
|
|
7182
7244
|
// it falls through to the existing conservative graph_get_subgraph path.
|
|
7183
|
-
|
|
7245
|
+
let targetProbe = await readPromotedTargetScope(ctx, nodeId, PROMOTED_PREFLIGHT_READ_OPTIONS);
|
|
7246
|
+
let targetScope = targetProbe.scope;
|
|
7247
|
+
const rootGraphIdentity = targetProbe.rootGraphIdentity;
|
|
7184
7248
|
// #2518 — a live root query names the current root workflow instance. Do not
|
|
7185
7249
|
// enter the promoted-subgraph identity path for an ordinary (or unproven)
|
|
7186
7250
|
// root node; a truncated StringConcatenate pinpoint used to fall through,
|
|
@@ -7236,6 +7300,23 @@ async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
|
|
|
7236
7300
|
return promotedSubgraphReadRefusal(widget, sub, "graph_get_subgraph could not determine whether the addressed node is a promoted container");
|
|
7237
7301
|
}
|
|
7238
7302
|
if (firstReadKind === "permanent") {
|
|
7303
|
+
// #2730 — a stale subgraph registry is a permanent-looking miss for an
|
|
7304
|
+
// ordinary root node. Refresh the mapping once and re-probe. Still
|
|
7305
|
+
// unverifiable → refuse. Do not spend the transient subgraph re-read on
|
|
7306
|
+
// a permanent application error.
|
|
7307
|
+
if (!mappingRefreshed && rootGraphIdentity !== null) {
|
|
7308
|
+
const refreshError = await refreshSubgraphMapping();
|
|
7309
|
+
if (refreshError)
|
|
7310
|
+
return refreshError;
|
|
7311
|
+
targetProbe = await readPromotedTargetScope(ctx, nodeId, PROMOTED_PREFLIGHT_READ_OPTIONS);
|
|
7312
|
+
targetScope = targetProbe.scope;
|
|
7313
|
+
if (targetScope?.activeView === "root" && targetScope.node === "ordinary") {
|
|
7314
|
+
const driftAfterMapping = panelBindingDriftReason(ctx, "after the subgraph mapping refresh", hasIdentityApi, identityBefore, tabBefore);
|
|
7315
|
+
if (driftAfterMapping)
|
|
7316
|
+
return ordinaryBindingRefusal(widget, driftAfterMapping);
|
|
7317
|
+
return ordinaryWritePlanFromScope(widget, targetScope, tabBefore, identityBefore);
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7239
7320
|
return promotedSubgraphReadRefusal(widget, sub, "graph_get_subgraph could not determine whether the addressed node is a promoted container");
|
|
7240
7321
|
}
|
|
7241
7322
|
// A binding/subgraph registration transition can make the first read
|
|
@@ -10002,7 +10083,21 @@ function refreshFenceFromOwnReply(ctx, reply) {
|
|
|
10002
10083
|
if (!uuid)
|
|
10003
10084
|
return null;
|
|
10004
10085
|
try {
|
|
10005
|
-
|
|
10086
|
+
const destPath = saveDestWorkflowPath(parsed);
|
|
10087
|
+
const destTab = destPath ? uniqueLiveTabForSaveDest(ctx, destPath) : undefined;
|
|
10088
|
+
const adopted = refreshWorkflowUuid(ctx, parsed);
|
|
10089
|
+
// #2768 — a SCOPE ctx.tabId never becomes dest. workflow_list stamps the
|
|
10090
|
+
// routed tab's advertised identity (#1815), so dest must hold the new uuid
|
|
10091
|
+
// or the next list/current-mode call is refused as a stale instance.
|
|
10092
|
+
if (destTab && destTab !== ctx.tabId) {
|
|
10093
|
+
try {
|
|
10094
|
+
ctx.bridge.refreshWorkflowUuid?.(destTab, uuid);
|
|
10095
|
+
}
|
|
10096
|
+
catch {
|
|
10097
|
+
// dest restamp is best-effort; the session fence is the ctx adoption
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
return adopted ? { status: "refreshed", uuid, before } : null;
|
|
10006
10101
|
}
|
|
10007
10102
|
catch {
|
|
10008
10103
|
return null; // never surface a throw here as worse than the existing fallback
|
|
@@ -10088,6 +10183,25 @@ function routingIsUnsavedPredecessor(ctx) {
|
|
|
10088
10183
|
const pin = ctx.workflowTarget?.get(ctx.tabId);
|
|
10089
10184
|
return typeof pin?.path === "string" && pin.path.startsWith("tmp:");
|
|
10090
10185
|
}
|
|
10186
|
+
/** The saved path this session was bound to before Save-As, if any. */
|
|
10187
|
+
function sessionSavedPath(ctx) {
|
|
10188
|
+
const pin = ctx.workflowTarget?.get(ctx.tabId);
|
|
10189
|
+
if (typeof pin?.path === "string" && pin.path.trim() && !pin.path.startsWith("tmp:")) {
|
|
10190
|
+
return canonicalSavedWorkflowPath(pin.path) ?? pin.path.trim();
|
|
10191
|
+
}
|
|
10192
|
+
return savedPathFromTabId(ctx.tabId) ?? undefined;
|
|
10193
|
+
}
|
|
10194
|
+
/**
|
|
10195
|
+
* #2768 — this Save-As replaced the canvas this session was editing.
|
|
10196
|
+
* `workflow_instance_changed` is the panel's own signal; dest path ≠ the
|
|
10197
|
+
* session's saved path is the same fact on a reply that omits the flag.
|
|
10198
|
+
*/
|
|
10199
|
+
function saveReplacedSessionCanvas(ctx, parsed, destPath) {
|
|
10200
|
+
if (parsed.workflow_instance_changed === true)
|
|
10201
|
+
return true;
|
|
10202
|
+
const from = sessionSavedPath(ctx);
|
|
10203
|
+
return Boolean(from && from !== destPath);
|
|
10204
|
+
}
|
|
10091
10205
|
function destPinnedTarget(pinned, destPath, parsed) {
|
|
10092
10206
|
let filename;
|
|
10093
10207
|
for (const v of [parsed.filename, parsed.workflow]) {
|
|
@@ -10123,9 +10237,14 @@ function adoptPinnedDestPath(ctx, destPath, parsed, destTab) {
|
|
|
10123
10237
|
* (`panel_set_todo`, `panel_canvas`) still address the old id.
|
|
10124
10238
|
*
|
|
10125
10239
|
* Follow dest when the current address is dead, when it already aliases onto
|
|
10126
|
-
* dest (same-socket tmp:→wf: / Save-As rename),
|
|
10240
|
+
* dest (same-socket tmp:→wf: / Save-As rename), when it is the unsaved
|
|
10127
10241
|
* predecessor of dest (pinned tmp: canvas whose first save published a
|
|
10128
|
-
* `workflows/…` routing key)
|
|
10242
|
+
* `workflows/…` routing key), or when this Save-As replaced the canvas this
|
|
10243
|
+
* session was editing (#2768 — dest path differs, or the panel set
|
|
10244
|
+
* `workflow_instance_changed`). Leaving the session on the source id after
|
|
10245
|
+
* that is a stale fence: dest is live, the source id may still canReach, and
|
|
10246
|
+
* panel_list_workflows / mode:"current" then fail with instance mismatch.
|
|
10247
|
+
* A live pin on a DIFFERENT saved tab that this save did not replace is left
|
|
10129
10248
|
* alone — that is #1917 / #884.
|
|
10130
10249
|
*
|
|
10131
10250
|
* Matching is on the dest path the save reply proved, and only when exactly
|
|
@@ -10149,7 +10268,10 @@ function repointRoutingAfterSave(ctx, reply) {
|
|
|
10149
10268
|
return;
|
|
10150
10269
|
}
|
|
10151
10270
|
const live = liveRoutingTab(ctx);
|
|
10152
|
-
const follow = !live ||
|
|
10271
|
+
const follow = !live ||
|
|
10272
|
+
live === destTab ||
|
|
10273
|
+
routingIsUnsavedPredecessor(ctx) ||
|
|
10274
|
+
saveReplacedSessionCanvas(ctx, parsed, destPath);
|
|
10153
10275
|
if (!follow)
|
|
10154
10276
|
return;
|
|
10155
10277
|
if (isScopeAddress(ctx.tabId)) {
|
|
@@ -13827,8 +13949,13 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets, onRunTicketOpen
|
|
|
13827
13949
|
// panel#1489 — reads are NOT refused here: they carry no unknown outcome,
|
|
13828
13950
|
// so they are dispatched on the bounded budget above instead.
|
|
13829
13951
|
const blocked = graphCmdBlockedByRunningPrompt(cmd);
|
|
13830
|
-
if (blocked)
|
|
13952
|
+
if (blocked) {
|
|
13953
|
+
// #2730 — a fenced mutation never reached the panel, so the subgraph
|
|
13954
|
+
// registry can stay stale until something walks the live graph. Mark
|
|
13955
|
+
// this tab so the next idle write refreshes mapping once.
|
|
13956
|
+
noteStaleSubgraphMapping(ctx.tabId);
|
|
13831
13957
|
return fail(blocked);
|
|
13958
|
+
}
|
|
13832
13959
|
// #2527 — a timed-out mutation may still be applying. Graph reads wait for
|
|
13833
13960
|
// that settlement (or disclose the outstanding receipt) so they cannot
|
|
13834
13961
|
// certify a stale widget value as current.
|
|
@@ -17660,7 +17787,7 @@ export function buildPanelToolDefs() {
|
|
|
17660
17787
|
return fail(err);
|
|
17661
17788
|
}
|
|
17662
17789
|
}),
|
|
17663
|
-
def("panel_connect", "Connect an output slot of one node to an input slot of another in the user's open graph. Slots accept a name ('MODEL', 'samples') or numeric index. If both slot args are omitted the panel picks the first type-compatible pairing. On failure the error lists every slot with its type and [connected] flag — re-check with panel_query_graph ({ids:[node_id], fields:'detail'}). LiteGraph wildcard-to-wildcard (`*` → `*`) pairings are compatible (a PrimitiveNode 'connect to widget input' output can land on LogicIF.when_true / when_false so the primitive becomes typed from the destination). A frontend PrimitiveNode only serializes through a target widget; connecting one to a forceInput-only / non-widget STRING is refused (panel_run would omit the required input) — use a backend STRING producer such as PrimitiveStringMultiline instead. Undoable.", {
|
|
17790
|
+
def("panel_connect", "Connect an output slot of one node to an input slot of another in the user's open graph. Slots accept a name ('MODEL', 'samples') or numeric index. If both slot args are omitted the panel picks the first type-compatible pairing. On failure the error lists every slot with its type and [connected] flag — re-check with panel_query_graph ({ids:[node_id], fields:'detail'}). LiteGraph wildcard-to-wildcard (`*` → `*`) pairings are compatible (a PrimitiveNode 'connect to widget input' output can land on LogicIF.when_true / when_false so the primitive becomes typed from the destination). An exposed subgraph INT rail (e.g. Scene Seed) is compatible with another INT widget input (LocalWildcardText.seed) — numeric widget min/max/step on the rail socket are not a different type. A frontend PrimitiveNode only serializes through a target widget; connecting one to a forceInput-only / non-widget STRING is refused (panel_run would omit the required input) — use a backend STRING producer such as PrimitiveStringMultiline instead. Undoable.", {
|
|
17664
17791
|
from_node_id: nodeId().describe("Source node id."),
|
|
17665
17792
|
from_output: slotRef
|
|
17666
17793
|
.optional()
|
|
@@ -17693,7 +17820,8 @@ export function buildPanelToolDefs() {
|
|
|
17693
17820
|
};
|
|
17694
17821
|
const call = (cmd, timeoutMs) => ctx.call(cmd, timeoutMs);
|
|
17695
17822
|
const connected = await retryConnectAgainstLiveGraph(connectArgs, call);
|
|
17696
|
-
const
|
|
17823
|
+
const afterRail = await retryRailSlotConnect(connectArgs, connected, call);
|
|
17824
|
+
const afterWildcard = await retryWildcardSlotConnect(connectArgs, afterRail, call);
|
|
17697
17825
|
return verifyPrimitiveForceInputAfterConnect(connectArgs, afterWildcard, call);
|
|
17698
17826
|
}),
|
|
17699
17827
|
def("panel_disconnect", "Disconnect an input slot of a node in the user's open graph. Undoable with Ctrl+Z.", {
|
|
@@ -20522,13 +20650,14 @@ export function buildPanelToolDefs() {
|
|
|
20522
20650
|
// attempting rebindWorkflowFence's independent workflow_list round trip,
|
|
20523
20651
|
// which can be refused by the exact fence this repairs.
|
|
20524
20652
|
//
|
|
20525
|
-
// #2419 — BEFORE the fence refresh so the stamp lands on the
|
|
20526
|
-
// address. Save-As mints a new tab id; the fence repair re-stamps
|
|
20527
|
-
// graph commands, but session-scoped commands (set_todo, graph_canvas
|
|
20528
|
-
// still address the old id unless routing is re-pointed.
|
|
20529
|
-
// when the current address is dead, aliases onto dest,
|
|
20530
|
-
// unsaved tmp: predecessor of dest
|
|
20531
|
-
//
|
|
20653
|
+
// #2419 / #2768 — BEFORE the fence refresh so the stamp lands on the
|
|
20654
|
+
// dest address. Save-As mints a new tab id; the fence repair re-stamps
|
|
20655
|
+
// graph commands, but session-scoped commands (set_todo, graph_canvas,
|
|
20656
|
+
// workflow_list) still address the old id unless routing is re-pointed.
|
|
20657
|
+
// Follow dest when the current address is dead, aliases onto dest, is
|
|
20658
|
+
// the unsaved tmp: predecessor of dest, or this Save-As replaced the
|
|
20659
|
+
// canvas this session was editing. A live pin on a different saved tab
|
|
20660
|
+
// that this save did not replace is left alone (#1917 / #884).
|
|
20532
20661
|
repointRoutingAfterSave(ctx, res);
|
|
20533
20662
|
const fenceRebind = refreshFenceFromOwnReply(ctx, res) ?? (await rebindWorkflowFence(ctx));
|
|
20534
20663
|
let canMutateNow;
|