comfyui-mcp 0.52.61 → 0.52.63

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.
@@ -814,6 +814,10 @@ export const __panelToolsTestHooks = {
814
814
  setRunLateAckGraceMs(ms) {
815
815
  runLateAckGraceMsOverride = ms;
816
816
  },
817
+ /** Shrink the #2078 save-timeout settle grace so a still-dirty follow-up does not wait 5s. */
818
+ setSaveTimeoutSettleGraceMs(ms) {
819
+ saveTimeoutSettleGraceMsOverride = ms;
820
+ },
817
821
  isRetrySafeCmd,
818
822
  isTransientReconnectError,
819
823
  // CivitAI sample-image gating (#623): the predicate that decides which results'
@@ -3615,30 +3619,87 @@ function saveLandedAfterTimeout(list) {
3615
3619
  const rec = active;
3616
3620
  return rec.modified === false && rec.persisted === true;
3617
3621
  }
3622
+ /**
3623
+ * A first save legitimately changes `tmp:<instance>` into `wf:<path>`. The
3624
+ * updated panel publishes the save command's eventual success under its exact
3625
+ * rid, so only that receipt can bridge the identity change. If the receipt field
3626
+ * is absent or does not match, the outcome remains unknown rather than crediting
3627
+ * a clean workflow that may belong to a tab the user switched to.
3628
+ */
3629
+ function saveProbeMatchesLateReceipt(saveRid, list) {
3630
+ if (!saveRid || !Array.isArray(list?.late_save_receipts))
3631
+ return false;
3632
+ if (!list?.active || typeof list.active !== "object" || Array.isArray(list.active))
3633
+ return false;
3634
+ const receipt = list.late_save_receipts.find((candidate) => candidate &&
3635
+ typeof candidate === "object" &&
3636
+ !Array.isArray(candidate) &&
3637
+ candidate.rid === saveRid &&
3638
+ candidate.cmd === "workflow_save");
3639
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt))
3640
+ return false;
3641
+ const result = receipt.result;
3642
+ if (!result || typeof result !== "object" || Array.isArray(result))
3643
+ return false;
3644
+ if (result.saved !== true)
3645
+ return false;
3646
+ // The panel's receipt is the save command's own eventual success. Still require
3647
+ // its produced workflow identity to match the currently observed active canvas;
3648
+ // a valid late save on A must not be credited after the user switched to B.
3649
+ return identityVerdict(result, list.active) === true;
3650
+ }
3618
3651
  const SAVE_TIMEOUT_OUTCOME_UNKNOWN = `\n\nOUTCOME UNKNOWN: the save was already in flight when this budget fired. ` +
3619
3652
  `Confirm by reading the saved file (or panel_list_workflows) before retrying — a re-issue may write twice.`;
3620
3653
  /**
3621
- * #2004the panel's 13s budget fired while userdata PUT was still running.
3622
- * A follow-up list that now shows modified:false persisted:true is the save
3623
- * completing; anything else stays outcome-unknown. persisted:true alone is
3624
- * the timeout-time snapshot of a previously-saved dirty tab, not proof.
3654
+ * #2078how long to keep reading tab state after the 13s save budget.
3655
+ *
3656
+ * #2004 already listed once. The reporter's PUT landed a moment later: that
3657
+ * snapshot was still dirty, the next panel_list_workflows was already clean.
3658
+ * Paid only on a path that has already spent its full bound. A canvas that
3659
+ * is clean on the first list returns immediately.
3660
+ */
3661
+ const SAVE_TIMEOUT_SETTLE_GRACE_MS = 5_000;
3662
+ const SAVE_TIMEOUT_SETTLE_POLL_MS = 200;
3663
+ let saveTimeoutSettleGraceMsOverride = null;
3664
+ function saveTimeoutSettleGraceMs() {
3665
+ return saveTimeoutSettleGraceMsOverride ?? SAVE_TIMEOUT_SETTLE_GRACE_MS;
3666
+ }
3667
+ function saveAckAfterTimeout(list) {
3668
+ return ok({
3669
+ saved: true,
3670
+ late_ack: true,
3671
+ acknowledged_after_timeout: true,
3672
+ active: list?.active,
3673
+ });
3674
+ }
3675
+ /**
3676
+ * #2004 / #2078 — the panel's 13s budget fired while userdata PUT was still
3677
+ * running. A list that shows modified:false persisted:true is the save
3678
+ * completing. One snapshot is not enough: the timeout-time canvas is still
3679
+ * dirty, and the PUT can land between that read and the caller's next list.
3680
+ * Keep reading until the grace, then stay outcome-unknown. persisted:true
3681
+ * alone is the timeout-time snapshot of a previously-saved dirty tab, not proof.
3625
3682
  */
3626
- async function settleWorkflowSaveTimeout(res, ctx) {
3683
+ async function settleWorkflowSaveTimeout(res, ctx, saveRid) {
3627
3684
  if (!isWorkflowSaveBudgetTimeout(res))
3628
3685
  return res;
3629
- try {
3630
- const listRes = await ctx.call({ cmd: "workflow_list" }, 6000);
3631
- const list = parseToolResultJson(listRes);
3632
- if (saveLandedAfterTimeout(list)) {
3633
- return ok({
3634
- saved: true,
3635
- acknowledged_after_timeout: true,
3636
- active: list?.active,
3637
- });
3686
+ const deadline = Date.now() + saveTimeoutSettleGraceMs();
3687
+ for (;;) {
3688
+ try {
3689
+ const listRes = await ctx.call({ cmd: "workflow_list" }, 6000);
3690
+ const list = parseToolResultJson(listRes);
3691
+ if (saveLandedAfterTimeout(list) &&
3692
+ saveProbeMatchesLateReceipt(saveRid, list)) {
3693
+ return saveAckAfterTimeout(list);
3694
+ }
3638
3695
  }
3639
- }
3640
- catch {
3641
- /* keep the timeout refusal; the note below is the honest remainder */
3696
+ catch {
3697
+ /* keep polling until the grace; the note below is the honest remainder */
3698
+ }
3699
+ const left = deadline - Date.now();
3700
+ if (left <= 0)
3701
+ break;
3702
+ await sleep(Math.max(1, Math.min(SAVE_TIMEOUT_SETTLE_POLL_MS, left)));
3642
3703
  }
3643
3704
  return appendToolResultText(res, SAVE_TIMEOUT_OUTCOME_UNKNOWN);
3644
3705
  }
@@ -8763,13 +8824,20 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets, onRunTicketOpen
8763
8824
  // here is the same recovery, just delayed until a canvas tab exists; it is
8764
8825
  // not a silent re-target.
8765
8826
  if (isScopeAddress(ctx.tabId)) {
8766
- if (ctx.pendingScopeRecoveryConsent) {
8827
+ const deferredConsent = ctx.pendingScopeRecoveryConsent === true ||
8828
+ (typeof bridge.hasScopeRecoveryConsent === "function" &&
8829
+ bridge.hasScopeRecoveryConsent(ctx.tabId));
8830
+ if (deferredConsent) {
8767
8831
  try {
8768
8832
  const out = rebindToActiveTab({ scopeRecoveryConsent: true });
8769
- if (out.rebound)
8833
+ if (out.rebound) {
8770
8834
  ctx.pendingScopeRecoveryConsent = false;
8771
- else if ((interactiveTabIds() ?? []).length > 0)
8835
+ bridge.clearScopeRecoveryConsent?.(ctx.tabId);
8836
+ }
8837
+ else if ((interactiveTabIds() ?? []).length > 0) {
8772
8838
  ctx.pendingScopeRecoveryConsent = false;
8839
+ bridge.clearScopeRecoveryConsent?.(ctx.tabId);
8840
+ }
8773
8841
  }
8774
8842
  catch {
8775
8843
  // still nothing bindable — keep the consent for the next attempt
@@ -13805,9 +13873,12 @@ export function buildPanelToolDefs() {
13805
13873
  if (ctx.awaitReachable && !(await ctx.awaitReachable())) {
13806
13874
  return noReachableTabFail(args.name ? "workflow_save_as" : "workflow_save", ctx);
13807
13875
  }
13876
+ let saveRid;
13808
13877
  const saveOnce = () => args.name
13809
13878
  ? ctx.call({ cmd: "workflow_save_as", name: args.name }, 15000)
13810
- : ctx.call({ cmd: "workflow_save" }, 15000);
13879
+ : ctx.call({ cmd: "workflow_save" }, 15000, (rid) => {
13880
+ saveRid = rid;
13881
+ });
13811
13882
  let res = await saveOnce();
13812
13883
  // #1710 — dest tab + dest nodes, extra still stamped as the save-as SOURCE.
13813
13884
  // Rebind extra to the confirmed dest, then retry the same save once.
@@ -13823,7 +13894,7 @@ export function buildPanelToolDefs() {
13823
13894
  }
13824
13895
  }
13825
13896
  if (res.isError && !args.name)
13826
- res = await settleWorkflowSaveTimeout(res, ctx);
13897
+ res = await settleWorkflowSaveTimeout(res, ctx, saveRid);
13827
13898
  if (res.isError) {
13828
13899
  // #1873 — named Save-As 409s by contract; the panel's "choose a different
13829
13900
  // name" is what forced a third file. Keep the 409 and name the rename path.
@@ -14032,10 +14103,12 @@ export function buildPanelToolDefs() {
14032
14103
  if (recoveringScope)
14033
14104
  ctx.bridge.endScopeRecovery?.(before);
14034
14105
  }
14035
- // panel#1557 — the consent survives a zero-tab DEFER so a graph call
14036
- // after the canvas reconnects can finish the recovery.
14037
- if (deferredBind && recoveringScope)
14106
+ // panel#1557 — the consent survives a zero-tab DEFER so a later graph
14107
+ // call (possibly on a fresh HTTP MCP ctx) can finish the recovery.
14108
+ if (deferredBind && recoveringScope) {
14038
14109
  ctx.pendingScopeRecoveryConsent = true;
14110
+ ctx.bridge.armScopeRecoveryConsent?.(before);
14111
+ }
14039
14112
  // Detect the rebind regardless of whether awaitReachable or rebindToActiveTab
14040
14113
  // performed it (either mutates ctx.tabId), so the note is never swallowed.
14041
14114
  if (!deferredBind && ctx.tabId !== before) {