comfyui-mcp 0.51.28 → 0.51.30

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.
@@ -65,7 +65,7 @@ function journalTabFor(ctx) {
65
65
  function journalConversationFor(ctx) {
66
66
  return conversationOfScopeAddress(ctx.tabId);
67
67
  }
68
- import { dispatchOutcomeOf, isCapabilityRefusal, isPanelCmdUnsupportedError, isReplyTimeoutTagged, isRoutingAmbiguity, } from "../services/ui-bridge.js";
68
+ import { BRIDGE_DEFAULT_TIMEOUT_MS, dispatchOutcomeOf, isCapabilityRefusal, isPanelCmdUnsupportedError, isReplyTimeoutTagged, isRoutingAmbiguity, } from "../services/ui-bridge.js";
69
69
  import { withWorkflowTarget, } from "../services/workflow-target-store.js";
70
70
  import { addUserMcpServer, readUserMcpServers, removeUserMcpServer, setUserMcpServerSecret, } from "../services/user-mcp-config.js";
71
71
  import { setComfyuiSecret, setAgentSecret, isAllowedAgentSecretKey, receiptDisclosures, shadowedNote, storeDamageNote, } from "../services/panel-secrets.js";
@@ -1609,6 +1609,135 @@ function isAckTimeout(res) {
1609
1609
  // prefixed acked error reach the receipt-recovery path.
1610
1610
  return /^(?:Error: )?Panel tab .+? did not reply to "workflow_open" within \d+\s*ms/i.test(text);
1611
1611
  }
1612
+ /**
1613
+ * #1468 — carry the bridge's AUTHORITATIVE reply-timeout marker across the
1614
+ * error → ToolResult conversion in `ctx.call`.
1615
+ *
1616
+ * Two codex rounds rejected deciding this from message TEXT, and both were right.
1617
+ * `isAckTimeout`'s looseness is safe for `workflow_open` because a match there
1618
+ * only opens a door — the panel's #514 receipt, correlated to the request's exact
1619
+ * rid, is what actually decides. A caller with no receipt has the predicate doing
1620
+ * all the work: match plus one root-looking read promotes an error to success. No
1621
+ * amount of regex tightening fixes that, because ACKED panel errors arrive as
1622
+ * ARBITRARY `msg.error` text (ui-bridge) and `ctx.call` flattens both kinds into
1623
+ * the same text-only result — so any sentence the bridge can write, a panel error
1624
+ * can also contain.
1625
+ *
1626
+ * `markReplyTimeout`/`isReplyTimeoutTagged` already answer the question exactly,
1627
+ * on the error object, at the only place that KNOWS: the bridge. The information
1628
+ * was simply being dropped in translation. This preserves it as a non-enumerable
1629
+ * symbol so the result's JSON payload is byte-identical and nothing downstream
1630
+ * can observe it by accident.
1631
+ */
1632
+ const REPLY_TIMEOUT_RESULT = Symbol("panel.replyTimeoutResult");
1633
+ function carryReplyTimeoutMark(err, res) {
1634
+ if (!isReplyTimeoutTagged(err))
1635
+ return res;
1636
+ Object.defineProperty(res, REPLY_TIMEOUT_RESULT, {
1637
+ value: true,
1638
+ enumerable: false,
1639
+ configurable: true,
1640
+ });
1641
+ return res;
1642
+ }
1643
+ /** True only for a ToolResult that `ctx.call` produced from a BRIDGE-TAGGED
1644
+ * reply timeout — the tab was reached and never answered. An acked executor
1645
+ * error can never carry this, whatever its text says. */
1646
+ function isReplyTimeoutResult(res) {
1647
+ return res?.isError === true && res[REPLY_TIMEOUT_RESULT] === true;
1648
+ }
1649
+ // ---- panel_exit_subgraph settle-after-ack-timeout (#1468) ------------------
1650
+ // `graph_exit_subgraph` timed out at 15 s while a following `panel_graph_outline`
1651
+ // proved the view HAD returned to root — an applied navigation reported as a
1652
+ // failed one. The bound is not the cause: the panel's own receipt
1653
+ // (`confirmCanvasNavigation`) budgets 25 polls × 40 ms ≈ 1 s and returns early on
1654
+ // success, so 15 s already clears its worst case by 15×. What fails is the ANSWER
1655
+ // getting back from a busy tab. The effect, though, is locally observable — so on
1656
+ // a no-reply we ask, exactly as #1473 does for the fence, instead of handing the
1657
+ // caller "may have been applied" and the homework.
1658
+ /** The one decisive observation: the canvas is at the root graph. */
1659
+ function exitConfirmedAtRootNote() {
1660
+ return (`CHECKED FOR YOU: the tab did not ACKNOWLEDGE the exit within the window, but a graph read ` +
1661
+ `taken immediately afterwards, on that same tab, reports the canvas at the ROOT graph — the ` +
1662
+ `state this tool exists to reach. No recovery step is needed and a retry would be wasted ` +
1663
+ `work. A missing acknowledgement is not evidence the navigation failed; here it is evidence ` +
1664
+ `the tab was too busy to answer in time. Stated precisely: what is established is WHERE THE ` +
1665
+ `CANVAS IS, not that this command is what put it there — someone navigating out on the ` +
1666
+ `canvas while the tab was unresponsive would read identically. Both leave you where you ` +
1667
+ `asked to be, so the distinction changes nothing you would do next; it is drawn because only ` +
1668
+ `one of the two was actually observed.`);
1669
+ }
1670
+ /**
1671
+ * The NON-decisive observation, stated as non-decisive. `graph_exit_subgraph`
1672
+ * pops to the IMMEDIATE PARENT, not to root (#412) — so "still inside a subgraph"
1673
+ * is equally consistent with the exit never landing and with the exit landing in
1674
+ * the parent of a NESTED subgraph. Recommending a retry on this reading would pop
1675
+ * a level the caller wanted to keep, which is the same class of harm as the false
1676
+ * failure being fixed, one step removed.
1677
+ */
1678
+ function exitInconclusiveInSubgraphNote(title) {
1679
+ const where = title ? `subgraph “${title}”` : "a subgraph";
1680
+ return (`CHECKED FOR YOU — and the check does NOT settle it. A graph read taken immediately after the ` +
1681
+ `missing acknowledgement reports the canvas inside ${where}. panel_exit_subgraph pops to the ` +
1682
+ `IMMEDIATE PARENT, not to the root graph, so this single observation cannot separate two ` +
1683
+ `cases: the exit never landed and you are where you started, OR the exit DID land and this is ` +
1684
+ `the parent you popped into from a nested subgraph. Settle it with a scope read ` +
1685
+ `(panel_graph_outline, or the canvas breadcrumb) before acting. If you do re-issue, use the ` +
1686
+ `retry_of token above rather than a bare repeat: a token names the original mutation and is ` +
1687
+ `answered from its ledger entry WITHOUT running the executor again (#694), whereas identical ` +
1688
+ `args with no token execute fresh — and from the second case that pops another level you may ` +
1689
+ `have wanted to keep.`);
1690
+ }
1691
+ /**
1692
+ * After an ack timeout on `graph_exit_subgraph`, take ONE scope read and report
1693
+ * what it found. Returns the untouched timeout when the read cannot answer —
1694
+ * #1473's rule: an unknown answer claims nothing in either direction, and reading
1695
+ * every probe failure as proof would invent a verdict out of a backgrounded tab.
1696
+ */
1697
+ async function settleExitSubgraphAfterAckTimeout(ctx, timedOut) {
1698
+ // The observation is only evidence about the tab the navigation was DISPATCHED
1699
+ // to (codex P1). `ctx.call` runs `ensureReachable` first, which silently rebinds
1700
+ // an unpinned current-mode session onto the sole remaining interactive tab when
1701
+ // the bound one has gone — the exact situation an unanswered command makes
1702
+ // likely. Without this the probe could read a DIFFERENT tab's canvas, and that
1703
+ // tab sitting at root would be reported as this navigation having landed: a
1704
+ // wrong-target success, which is worse than the false failure being fixed.
1705
+ const dispatchTab = ctx.tabId;
1706
+ // `fields:"ids", limit:1` is the cheapest shape that still carries `viewing` —
1707
+ // the panel builds that field unconditionally on every graph_query return path.
1708
+ const probe = await ctx.call({ cmd: "graph_query", fields: "ids", limit: 1 }, 8000);
1709
+ // Checked AFTER the call, because the rebind happens inside it. A moved binding
1710
+ // makes the reading inconclusive, not false — so it takes the same "claim
1711
+ // nothing" exit as an unanswerable probe.
1712
+ if (ctx.tabId !== dispatchTab)
1713
+ return timedOut;
1714
+ const viewing = parseToolResultJson(probe)?.viewing;
1715
+ const scope = typeof viewing?.scope === "string" ? viewing.scope : null;
1716
+ if (scope === "root") {
1717
+ // `at_root`, NOT `exited`. The read proves WHERE THE CANVAS IS; it cannot
1718
+ // prove this command is what put it there (the user may have navigated out on
1719
+ // the canvas while the tab was too busy to answer us). Those happen to be the
1720
+ // same actionable answer — the caller's goal state holds either way — but only
1721
+ // one of them is something the observation actually establishes, and naming
1722
+ // the stronger claim would be this issue's own defect pointed the other way.
1723
+ return ok({
1724
+ viewing,
1725
+ at_root: true,
1726
+ acknowledged: false,
1727
+ confirmed_by: "graph read after ack timeout",
1728
+ note: exitConfirmedAtRootNote(),
1729
+ });
1730
+ }
1731
+ if (scope === "subgraph") {
1732
+ const title = typeof viewing?.title === "string" && viewing.title ? viewing.title : null;
1733
+ const text = timedOut.content?.find((c) => c.type === "text")?.text ?? "";
1734
+ return {
1735
+ ...timedOut,
1736
+ content: [{ type: "text", text: `${text}\n\n${exitInconclusiveInSubgraphNote(title)}` }],
1737
+ };
1738
+ }
1739
+ return timedOut;
1740
+ }
1612
1741
  /** Parse a ctx.call ToolResult's text payload as JSON, or null if not parseable. */
1613
1742
  function parseToolResultJson(res) {
1614
1743
  if (!res || res.isError)
@@ -3726,6 +3855,7 @@ panelGapNote = "") {
3726
3855
  if (fenceStillMatches) {
3727
3856
  return {
3728
3857
  binding: "not_recovered",
3858
+ settleByRead: true,
3729
3859
  note: `${lead}. This session's fence was NOT re-derived, and whether that reply describes ` +
3730
3860
  `the LIVE canvas is exactly what could not be confirmed — a stale or background ` +
3731
3861
  `record can carry the right uuid and still not be the canvas in front of the user, ` +
@@ -5421,7 +5551,13 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
5421
5551
  `restart/reload. Wait a moment and retry; if it persists, rebind with ` +
5422
5552
  `panel_set_workflow_target({mode:"current"}). (${err2 instanceof Error ? err2.message : String(err2)})`);
5423
5553
  }
5424
- return fail(err2);
5554
+ // #1468 — the RETRY's own failure can be a reply timeout too: this branch
5555
+ // is only entered when the FIRST error was a reconnect flap or a switch
5556
+ // refusal, so a tagged no-reply lands here rather than on the outer path.
5557
+ // Leaving it unmarked fails closed (nothing is settled, no false success)
5558
+ // but silently switches the settle off for a real sequence, which is the
5559
+ // kind of gap that reads as "the fix does not work" much later.
5560
+ return carryReplyTimeoutMark(err2, fail(err2));
5425
5561
  }
5426
5562
  }
5427
5563
  // #442 defect 4: a MUTATING command (deliberately excluded from RETRY_SAFE_CMDS)
@@ -5653,9 +5789,9 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
5653
5789
  RETRY_TOKEN_CMDS.has(typeof cmd.cmd === "string" ? cmd.cmd : "") &&
5654
5790
  (dispatchOutcomeOf(err) === true || isReplyTimeoutTagged(err))) {
5655
5791
  const cause = err instanceof Error ? err.message : String(err);
5656
- return fail(`${cause}\n\nTo retry this exact mutation, re-issue identical args plus retry_of:"${dispatchedRid}"; otherwise call normally.`);
5792
+ return carryReplyTimeoutMark(err, fail(`${cause}\n\nTo retry this exact mutation, re-issue identical args plus retry_of:"${dispatchedRid}"; otherwise call normally.`));
5657
5793
  }
5658
- return fail(err);
5794
+ return carryReplyTimeoutMark(err, fail(err));
5659
5795
  }
5660
5796
  };
5661
5797
  // Human-in-the-loop confirmation for a DESTRUCTIVE op: render a yes/no card in
@@ -8739,7 +8875,27 @@ export function buildPanelToolDefs() {
8739
8875
  // other direct-bridge call site) — without this an orphaned session
8740
8876
  // wrongly returns "no connected tab" even when a live tab exists (#381).
8741
8877
  ctx.ensureReachable?.();
8742
- const reply = await ctx.bridge.send({ cmd: "civitai_search", query, filters: args.filters, browsingLevels }, { tabId: ctx.tabId, timeoutMs: 10000 });
8878
+ // #1468 was a hardcoded 10_000, HALF this codebase's own default, and
8879
+ // the tight bound is the entire bug: `civitai_search` kept timing out
8880
+ // while the search demonstrably applied (renderRev advanced,
8881
+ // civitai_results reported loading:true), so an applied mutation was
8882
+ // reported to the caller as a failure.
8883
+ //
8884
+ // The tight bound was there to fence a wait on CivitAI's HTTP. That wait
8885
+ // is GONE: panel #282 made `driveSearch` resolve on DISPATCH — it fires
8886
+ // `void reload(...)` and returns `{dispatched:true, renderRev}` with no
8887
+ // await anywhere in the handler. That shipped at panel 0.11.0; #1468 was
8888
+ // filed from 0.11.44, so the reporter already had it. Measured, not
8889
+ // assumed — my own first reading of this issue asserted the coupling was
8890
+ // still there and argued AGAINST raising the bound on that basis.
8891
+ //
8892
+ // With nothing external left to wait on, what remains between the panel's
8893
+ // return and the caller's error is only whether the reply gets back in
8894
+ // time — the #357/#694 shape, a busy-but-alive main thread missing a
8895
+ // tighter-than-default bound and succeeding moments later. So this takes
8896
+ // the shared default like every other panel command, rather than keeping
8897
+ // a fence around a wait that no longer exists.
8898
+ const reply = await ctx.bridge.send({ cmd: "civitai_search", query, filters: args.filters, browsingLevels }, { tabId: ctx.tabId, timeoutMs: BRIDGE_DEFAULT_TIMEOUT_MS });
8743
8899
  // Do NOT let a supplied-but-unapplied creator filter masquerade as a
8744
8900
  // legitimate empty result: if the panel echoes back a different (or null)
8745
8901
  // creator, surface an explicit warning so the caller can tell "filter
@@ -9197,10 +9353,95 @@ export function buildPanelToolDefs() {
9197
9353
  const fence = fenceRebind
9198
9354
  ? describeFenceRebind(fenceRebind, canMutateNow, refusalCause)
9199
9355
  : undefined;
9356
+ // #1473 — TAKE THE ADVICE THIS MESSAGE GIVES, instead of assigning it as homework.
9357
+ //
9358
+ // The reporter restarted ComfyUI, called this, was told the binding was NOT
9359
+ // restored — and their very next `panel_graph_outline` succeeded with the expected
9360
+ // graph. #1401 already made the message right for this shape: when the panel's
9361
+ // reported identity MATCHES the fence this session already holds, it says so, says
9362
+ // it is NOT a claim that graph tools work, and prescribes a cheap graph read to
9363
+ // settle it. All true. It just left the caller to run that read themselves.
9364
+ //
9365
+ // So the read is taken here, on the matching-uuid path ONLY, and its ANSWER is put
9366
+ // in the message.
9367
+ //
9368
+ // THE RESULT STILL REPORTS FAILURE, deliberately, and that is #1401's call not
9369
+ // mine: "softening the DIAGNOSIS must not soften the RESULT — the rebind genuinely
9370
+ // did not happen". It did not. What changes is that the caller no longer has to
9371
+ // discover, one call later, that nothing was wrong: the answer arrives with the
9372
+ // refusal instead of after it. Whether this should become a SUCCESS is a real
9373
+ // product question and a reversal of a reasoned decision, so it is raised on the
9374
+ // issue rather than decided here.
9375
+ let settledNote = "";
9376
+ if (fence && fence.binding === "not_recovered" && fence.settleByRead) {
9377
+ // WHAT THE PROBE PROVES, AND ONLY THAT (codex).
9378
+ //
9379
+ // Two ways to overclaim here, and the first version did both:
9380
+ //
9381
+ // • EVERY error read as "refused". `ctx.call` turns a transport failure into an
9382
+ // error result too, so a `graph_query` that merely TIMED OUT on a backgrounded
9383
+ // tab would have been reported as a confirmed fence refusal — inventing a wedge
9384
+ // out of a slow tab, and contradicting this block's own "unknown says nothing"
9385
+ // rule. Only an actual instance-mismatch refusal proves the fence rejected it.
9386
+ // • A passing READ read as "graph tools work". The write fence is a SEPARATE
9387
+ // capability: a panel can serve reads while refusing every mutation, and the
9388
+ // capability probe above already knows. So the claim is scoped to reads, and the
9389
+ // known-negative write case is stated rather than papered over.
9390
+ let probeRefused;
9391
+ let probeOk = false;
9392
+ try {
9393
+ // The cheapest fenced read there is: ids only, one row. It is refused by the
9394
+ // same instance fence every graph command carries, which is exactly the
9395
+ // question — a full outline would answer it no better and would cost the caller
9396
+ // a page of graph on a recovery path.
9397
+ const probe = await ctx.call({ cmd: "graph_query", fields: "ids", limit: 1 }, 8000);
9398
+ if (!probe.isError)
9399
+ probeOk = true;
9400
+ else
9401
+ probeRefused = isWorkflowInstanceMismatch(toolResultText(probe));
9402
+ }
9403
+ catch (err) {
9404
+ probeRefused = isWorkflowInstanceMismatch(err);
9405
+ }
9406
+ if (probeOk) {
9407
+ settledNote =
9408
+ // ONE OBSERVATION, STATED AS ONE (codex r2). A passing graph_query proves that
9409
+ // read passed this fence just now — not that every command will, and not that
9410
+ // the cause "was" a race. The earlier wording generalised to "READS work" and
9411
+ // asserted the cause outright, which is the same overclaim this reply exists to
9412
+ // stop making.
9413
+ `
9414
+
9415
+ CHECKED FOR YOU: the graph read this message prescribes was just run — a ` +
9416
+ `graph_query against this fence — and it SUCCEEDED. So the fence did not reject ` +
9417
+ `THAT command a moment ago, which is the single fact this settles: it is evidence ` +
9418
+ `of a reconciliation race after the reconnect rather than a broken binding, not a ` +
9419
+ `guarantee about the next command.` +
9420
+ (canMutateNow === false
9421
+ ? ` MUTATIONS are a separate matter and remain refused on this tab — that is the ` +
9422
+ `write-fence capability above, not the binding, and re-running this will not ` +
9423
+ `change it.`
9424
+ : ` Nothing suggests a recovery step is needed for the binding; the next graph ` +
9425
+ `command is still the authority.`) +
9426
+ ` (The rebind itself still did not happen, which is why this is reported as a ` +
9427
+ `failure.)`;
9428
+ }
9429
+ else if (probeRefused === true) {
9430
+ settledNote =
9431
+ `
9432
+
9433
+ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
9434
+ `was REFUSED by the instance fence — so the reply was stale after all and graph ` +
9435
+ `commands really are being rejected. The remedy above applies.`;
9436
+ }
9437
+ // Anything else — a timeout, a dropped tab, an error that is not a fence refusal —
9438
+ // settles NOTHING and says nothing. An inconclusive probe must not become evidence
9439
+ // in either direction.
9440
+ }
9200
9441
  if (fence && fence.binding === "not_recovered") {
9201
9442
  return fail(`panel_set_workflow_target({mode:"current"}) did NOT restore this session's graph ` +
9202
9443
  `binding.\n\nAPPLIED (do not repeat this part): the workflow target is now ` +
9203
- `mode:"current"${rebindNote ? `.${rebindNote}` : "."}\n\nNOT APPLIED:${fence.note}`);
9444
+ `mode:"current"${rebindNote ? `.${rebindNote}` : "."}\n\nNOT APPLIED:${fence.note}${settledNote}`);
9204
9445
  }
9205
9446
  // #888 — SAY what the scope repin did. A silent success is as unhelpful
9206
9447
  // here as the silent refusal #1077 fixed: the whole complaint is that
@@ -9421,7 +9662,18 @@ export function buildPanelToolDefs() {
9421
9662
  }
9422
9663
  }),
9423
9664
  def("panel_enter_subgraph", "Navigate INTO a subgraph node so you can read and EDIT its inner nodes — after this, panel_query_graph / panel_graph_outline and all panel_* edit tools target the subgraph's inner graph (the user sees the canvas drill in). This is how you edit inside a subgraph (e.g. tweak a widget on an inner node). Call panel_exit_subgraph when done. Returns the new viewing scope.", { node_id: nodeId().describe("Subgraph node id (is_subgraph=true).") }, async (args, ctx) => ctx.call({ cmd: "graph_enter_subgraph", node_id: args.node_id }, 15000)),
9424
- def("panel_exit_subgraph", "Leave the current subgraph and return to the root graph (undo a panel_enter_subgraph). After this, panel_* tools target the root graph again.", {}, async (_args, ctx) => ctx.call({ cmd: "graph_exit_subgraph" }, 15000)),
9665
+ def("panel_exit_subgraph", "Leave the current subgraph and return to the root graph (undo a panel_enter_subgraph). After this, panel_* tools target the root graph again.", {}, async (_args, ctx) => {
9666
+ const res = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
9667
+ // #1468 — ONLY a no-reply is settled by a read. A genuine executor error
9668
+ // (the panel's own "could not confirm … no observation ever saw the canvas
9669
+ // there") is an ACKED reply the bridge received and relayed; it already
9670
+ // reasoned about this exact uncertainty and prescribes its own next step,
9671
+ // so re-deciding it from out here would overwrite a better-informed verdict
9672
+ // with a worse-informed one.
9673
+ if (!isReplyTimeoutResult(res))
9674
+ return res;
9675
+ return settleExitSubgraphAfterAckTimeout(ctx, res);
9676
+ }),
9425
9677
  def("panel_move_rail", "Reposition a subgraph's input or output RAIL (the boundary I/O node that the inner wires connect to). You MUST be INSIDE the subgraph first (panel_enter_subgraph). Read current rail positions from panel_query_graph's `rails` field (present when viewing a subgraph). Use this to place the input rail just left of the first node column and the output rail just right of the last one, so a tidy interior layout doesn't leave the rails stranded. rail is 'input' or 'output'.", {
9426
9678
  rail: z.enum(["input", "output"]).describe("Which boundary rail to move."),
9427
9679
  pos: xy().describe("New top-left [x, y] (two numbers)."),