comfyui-mcp 0.52.62 → 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
  }
@@ -13812,9 +13873,12 @@ export function buildPanelToolDefs() {
13812
13873
  if (ctx.awaitReachable && !(await ctx.awaitReachable())) {
13813
13874
  return noReachableTabFail(args.name ? "workflow_save_as" : "workflow_save", ctx);
13814
13875
  }
13876
+ let saveRid;
13815
13877
  const saveOnce = () => args.name
13816
13878
  ? ctx.call({ cmd: "workflow_save_as", name: args.name }, 15000)
13817
- : ctx.call({ cmd: "workflow_save" }, 15000);
13879
+ : ctx.call({ cmd: "workflow_save" }, 15000, (rid) => {
13880
+ saveRid = rid;
13881
+ });
13818
13882
  let res = await saveOnce();
13819
13883
  // #1710 — dest tab + dest nodes, extra still stamped as the save-as SOURCE.
13820
13884
  // Rebind extra to the confirmed dest, then retry the same save once.
@@ -13830,7 +13894,7 @@ export function buildPanelToolDefs() {
13830
13894
  }
13831
13895
  }
13832
13896
  if (res.isError && !args.name)
13833
- res = await settleWorkflowSaveTimeout(res, ctx);
13897
+ res = await settleWorkflowSaveTimeout(res, ctx, saveRid);
13834
13898
  if (res.isError) {
13835
13899
  // #1873 — named Save-As 409s by contract; the panel's "choose a different
13836
13900
  // name" is what forced a third file. Keep the 409 and name the rename path.