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.
@@ -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 parseVerifiedQueriedNodeScope(payload, nodeId);
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
- const targetScope = await readPromotedTargetScope(ctx, nodeId, PROMOTED_PREFLIGHT_READ_OPTIONS);
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
- return refreshWorkflowUuid(ctx, parsed) ? { status: "refreshed", uuid, before } : null;
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), or when it is the unsaved
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). A live pin on a DIFFERENT saved tab is left
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 || live === destTab || routingIsUnsavedPredecessor(ctx);
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 afterWildcard = await retryWildcardSlotConnect(connectArgs, connected, call);
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 dest
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. Follow dest
20529
- // when the current address is dead, aliases onto dest, or is the
20530
- // unsaved tmp: predecessor of dest. A live pin on a different saved
20531
- // tab is left alone (#1917 / #884).
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;