comfyui-mcp 0.50.70 → 0.50.72

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.
@@ -4704,8 +4704,21 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
4704
4704
  if (!opts?.scopeRecoveryConsent || !pinProvenDead) {
4705
4705
  return { previous, current: previous, rebound: false };
4706
4706
  }
4707
- const repinned = bridge.repinScopeToActive?.(previous);
4708
- return { previous, current: previous, rebound: Boolean(repinned) };
4707
+ // #1077 Finding 2 — carry the REASON out when nothing was repinned. This
4708
+ // collapsed to `Boolean(repinned)`, so four different refusals arrived at
4709
+ // the caller as one bare `false` and the user was told only that the
4710
+ // adoption was refused. One of those refusals repeats forever (an active
4711
+ // tab on another backend's conversation while this one has several), and
4712
+ // nothing in the reply hinted that naming a workflow is the way out.
4713
+ const outcome = bridge.repinScopeToActive?.(previous);
4714
+ const repinned = typeof outcome === "string" ? outcome : undefined;
4715
+ const repinRefusal = outcome && typeof outcome === "object" ? outcome.reason : undefined;
4716
+ return {
4717
+ previous,
4718
+ current: previous,
4719
+ rebound: Boolean(repinned),
4720
+ ...(repinRefusal ? { repinRefusal } : {}),
4721
+ };
4709
4722
  }
4710
4723
  // A healthy binding is left untouched (never disturb a live session). Recovery only
4711
4724
  // fires for an orphaned/stale tab id.
@@ -5290,11 +5303,26 @@ function recoveredAskResult(entry) {
5290
5303
  * the question at hand, which is the misattribution this whole path exists to
5291
5304
  * prevent. Reporting them beats swallowing them — the user did answer something.
5292
5305
  */
5293
- function askTimeoutResult(tabId, fingerprint, recovery) {
5294
- let text = "The question card was not answered in time (or no interactive panel surface " +
5295
- "rendered ite.g. an exec/headless run), so nothing was selected. If you " +
5296
- "still need the decision, ask the user directly in plain chat text, or " +
5297
- "re-invoke panel_ask from an interactive ComfyUI tab.";
5306
+ export function askTimeoutResult(tabId, fingerprint, recovery,
5307
+ /**
5308
+ * #1243 did a reachability check actually run before the wait? When it did, the
5309
+ * "maybe nothing rendered the card" alternative has ALREADY been ruled out, and
5310
+ * offering it anyway sends the agent to re-invoke from "an interactive tab" it was
5311
+ * demonstrably already on. `ctx.ensureReachable` is optional-chained at the call
5312
+ * site, so this is threaded rather than assumed — an absent check means the
5313
+ * alternative is still live and is still named.
5314
+ */
5315
+ surfaceConfirmed) {
5316
+ const waited = `${Math.round(ASK_TOTAL_BUDGET_CAP_MS / 1000)}s`;
5317
+ let text = surfaceConfirmed
5318
+ ? `The question card was delivered to the panel and went unanswered for ${waited}, ` +
5319
+ "so nothing was selected — this is a timeout, not a delivery failure. The user " +
5320
+ "may simply not have been at the screen. If you still need the decision, ask them " +
5321
+ "directly in plain chat text, or re-invoke panel_ask when they are present."
5322
+ : `The question card was not answered within ${waited} — either it went unanswered, ` +
5323
+ "or no interactive panel surface rendered it (e.g. an exec/headless run), and this " +
5324
+ "call could not establish which. If you still need the decision, ask the user " +
5325
+ "directly in plain chat text, or re-invoke panel_ask from an interactive ComfyUI tab.";
5298
5326
  // What is surfaced, and why each:
5299
5327
  // • an answer that reached NO tool call — nobody has it, so it must be told;
5300
5328
  // • an answer to THIS EXACT question that DID reach a tool call but could not
@@ -5454,7 +5482,13 @@ async function askUserWithGrace(ctx, ask) {
5454
5482
  // a conversation replaced mid-ask would let the live turn's ack settle a
5455
5483
  // warning that conversation never saw — the debt path reaching a live turn
5456
5484
  // through a helper, which is how it slipped the gate before.
5457
- const body = askTimeoutResult(tabId, fingerprint, outcome.recovery);
5485
+ // #1243 derive this rather than hardcoding `true`. The reachability call above
5486
+ // is optional-chained (`ctx.ensureReachable?.()`), so on a ctx that does not
5487
+ // provide it NO check ran and the no-surface alternative is still live. Passing a
5488
+ // literal would have made the false branch unreachable in production — dead code
5489
+ // justified by a hypothetical, and a test exercising a path nothing takes.
5490
+ const surfaceConfirmed = typeof ctx.ensureReachable === "function";
5491
+ const body = askTimeoutResult(tabId, fingerprint, outcome.recovery, surfaceConfirmed);
5458
5492
  return AskAnswers.askBelongsToLiveConversation(askId)
5459
5493
  ? withDroppedAnswerWarning(tabId, body)
5460
5494
  : body;
@@ -7469,7 +7503,7 @@ export function buildPanelToolDefs() {
7469
7503
  .min(1)
7470
7504
  .describe("Wizard step/field refs to glow green (as the wizard labels them). Pass several to highlight a set."),
7471
7505
  }, async (args, ctx) => ctx.call({ cmd: "training_highlight", refs: args.refs }, 10000)),
7472
- def("panel_ask", "Ask the user to choose between options — renders an interactive question card in the panel chat and BLOCKS until they pick, returning their choice as text. Use this (NOT the AskUserQuestion tool, which never renders here) whenever you need the user to decide between options. Each option may carry a short description. The card always includes an 'Other…' free-text field, so the returned string may be a listed label or whatever the user typed (comma-joined for multi_select). Ask only when the answer genuinely changes what you do.", {
7506
+ def("panel_ask", "Ask the user to choose between options — renders an interactive question card in the panel chat and BLOCKS until they pick, returning their choice as text. It does NOT block forever: if nobody answers within ~285s the call returns an error saying so, and you must then ask in plain chat text or re-invoke when the user is present. Budget for that — a question asked while the user is away costs the better part of five minutes before you learn anything. Use this (NOT the AskUserQuestion tool, which never renders here) whenever you need the user to decide between options. Each option may carry a short description. The card always includes an 'Other…' free-text field, so the returned string may be a listed label or whatever the user typed (comma-joined for multi_select). Ask only when the answer genuinely changes what you do.", {
7473
7507
  question: z.string().describe("The question to ask, e.g. 'Which sampler should I use?'"),
7474
7508
  options: z
7475
7509
  .array(z.object({
@@ -7603,7 +7637,16 @@ export function buildPanelToolDefs() {
7603
7637
  // THE explicit scope-recovery consent (#884 gate 3) — the only
7604
7638
  // caller that may escape a DEAD scope pin (a healthy pin still
7605
7639
  // stays put; see rebindToActiveTab's double gate).
7606
- ctx.rebindToActiveTab({ scopeRecoveryConsent: true });
7640
+ const rebind = ctx.rebindToActiveTab({ scopeRecoveryConsent: true });
7641
+ // #1077 Finding 2 — a scope repin that declined now says WHY, and
7642
+ // this is where the user reads it. The refusal used to be a bare
7643
+ // boolean, so a session stuck in the one state that repeats forever
7644
+ // (the active tab belongs to another backend's conversation while
7645
+ // this one has several eligible tabs) saw no difference from a
7646
+ // healthy pin being correctly left alone.
7647
+ if (rebind?.repinRefusal) {
7648
+ rebindNote += ` The session pin was NOT moved: ${rebind.repinRefusal}.`;
7649
+ }
7607
7650
  }
7608
7651
  catch (err) {
7609
7652
  // #474: with 2+ live tabs the rebind is AMBIGUOUS — fail so the user picks.