comfyui-mcp 0.52.82 → 0.52.84

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.
@@ -190,7 +190,7 @@ import { RunCompletions } from "./run-completion-journal.js";
190
190
  import { AskAnswers, askFingerprint, PANEL_ASK_ID_PREFIX, } from "./ask-answer-journal.js";
191
191
  import { getQueueVerified, resetClient, resetObjectInfoCache, } from "../comfyui/client.js";
192
192
  import { convertUiToApi, collectNodeTypes } from "../services/workflow-converter.js";
193
- import { restartComfyUI, preflightLocalRestart, readServingArgv, describeArgvDrift, recordRestartDispatch, clearRestartDispatch, getRestartDispatchRecord, RESTART_DISPATCH_CAUSATION_WINDOW_MS, PROCESS_WIDE_RESTART_DISPATCH_TOKEN, __processControlTestHooks, } from "../services/process-control.js";
193
+ import { restartComfyUI, preflightLocalRestart, readServingArgv, resolveVerifiedProxyRestartTarget, describeArgvDrift, recordRestartDispatch, clearRestartDispatch, getRestartDispatchRecord, RESTART_DISPATCH_CAUSATION_WINDOW_MS, PROCESS_WIDE_RESTART_DISPATCH_TOKEN, __processControlTestHooks, } from "../services/process-control.js";
194
194
  import { resetManagerApiCache } from "../services/manager-api-cache.js";
195
195
  import { desktopSavedLaunchArgs, describeSavedLaunchArgDrift, } from "../services/desktop-launch-args.js";
196
196
  import { config, isRemoteMode, isCloudMode, getBootLocalComfyUIBaseUrl, getComfyUIBaseUrl, getComfyuiTargetGeneration, } from "../config.js";
@@ -735,6 +735,10 @@ export const __panelToolsTestHooks = {
735
735
  setLocalRestartPreflight(fn) {
736
736
  localRestartPreflightOverride = fn;
737
737
  },
738
+ /** Inject the narrow EZi/CEI proxy proof; null restores the live resolver. */
739
+ setVerifiedProxyRestartTarget(fn) {
740
+ verifiedProxyRestartTargetOverride = fn;
741
+ },
738
742
  /** Inject a fast #742 decline-probe recheck window so decline-path tests
739
743
  * don't wait the real ~6s. null restores the DECLINE_PROBE_* constants. */
740
744
  setDeclineProbeTiming(timing) {
@@ -2221,6 +2225,16 @@ let healthProbeOverride = null;
2221
2225
  /** Test injection for the #742 refuse-safe restart preflight (the real one is
2222
2226
  * preflightLocalRestart in process-control). null → the live preflight. */
2223
2227
  let localRestartPreflightOverride = null;
2228
+ let verifiedProxyRestartTargetOverride = null;
2229
+ function panelVerifiedProxyRestartTarget() {
2230
+ return (verifiedProxyRestartTargetOverride ?? resolveVerifiedProxyRestartTarget)();
2231
+ }
2232
+ function proxyTargetBindsPanel(ctx, target) {
2233
+ return (target != null &&
2234
+ sameHttpBase(getComfyUIBaseUrl(), target.proxyBase) &&
2235
+ ctx.bridge?.tabIsLocal?.(ctx.tabId) === true &&
2236
+ sameHttpBase(ctx.bridge?.tabServerOrigin?.(ctx.tabId), target.proxyBase));
2237
+ }
2224
2238
  /** Test injection for the #742 decline-probe recheck window, so tests don't
2225
2239
  * wait the real ~6s. null → the DECLINE_PROBE_* constants. */
2226
2240
  let declineProbeTimingOverride = null;
@@ -5679,10 +5693,22 @@ function isRetryableRunToNodeStampRace(res, rejection) {
5679
5693
  return (/workflow graph CHANGED after the run was queued/i.test(text) &&
5680
5694
  /Nothing was queued/i.test(text));
5681
5695
  }
5696
+ /**
5697
+ * #2120 — an older panel can decorate a fixed seed while it builds the deferred
5698
+ * run prompt. That can make the same scoped run hit the certified stamp race more
5699
+ * than once. The panel still has to identify the changed entry as a seed; this is
5700
+ * deliberately narrower than allowing an arbitrary third dispatch.
5701
+ */
5702
+ const MAX_SCOPED_STAMP_RACE_RETRIES = 2;
5703
+ function isSeedRunToNodeStampRace(res, rejection) {
5704
+ return (isRetryableRunToNodeStampRace(res, rejection) &&
5705
+ /\bdiffering\b[\s\S]{0,160}\bseed\b/i.test(toolResultText(rejection)));
5706
+ }
5682
5707
  export const __panelRunTestHooks = {
5683
5708
  detectRunRejection,
5684
5709
  formatRunRejection,
5685
5710
  isRetryableRunToNodeStampRace,
5711
+ isSeedRunToNodeStampRace,
5686
5712
  describeDroppedOutputs,
5687
5713
  };
5688
5714
  /** Drop a trailing .json (case-insensitive) so filename/path forms compare equal. */
@@ -13262,15 +13288,25 @@ export function buildPanelToolDefs() {
13262
13288
  // "you'll be notified automatically" guidance. Only a genuine queue
13263
13289
  // gets the anti-poll note below.
13264
13290
  let rejection = detectRunRejection(res);
13265
- // #772 — ONE re-issue, and only for the scoped-run stamp race the panel
13266
- // explicitly certifies as not-queued. Gated on OUR OWN args (this really was a
13267
- // scoped run) and on the panel having ANSWERED, so an unknown outcome is never
13268
- // retried. Exactly once: a second race is surfaced, never re-raced.
13291
+ // #772/#2120 — re-issue only the scoped-run stamp race the panel explicitly
13292
+ // certifies as not-queued. Gated on OUR OWN args (this really was a scoped run)
13293
+ // and on the panel having ANSWERED, so an unknown outcome is never retried.
13294
+ // The ordinary race gets one re-issue. A repeated race gets ONE additional
13295
+ // chance only when the panel names a differing seed entry — the fixed-seed
13296
+ // decoration race reported in #2120. Every attempt still passes the panel's
13297
+ // graph-stamp check independently; this is not a bypass or a blind mutation
13298
+ // retry.
13269
13299
  let scopeRebuilt = false;
13270
- if (rejection &&
13300
+ let scopeRetryCount = 0;
13301
+ while (rejection &&
13271
13302
  typeof args.to_node_id === "number" &&
13272
- isRetryableRunToNodeStampRace(res, rejection)) {
13303
+ isRetryableRunToNodeStampRace(res, rejection) &&
13304
+ (scopeRetryCount === 0 ||
13305
+ (scopeRetryCount < MAX_SCOPED_STAMP_RACE_RETRIES &&
13306
+ isSeedRunToNodeStampRace(res, rejection)))) {
13273
13307
  scopeRebuilt = true;
13308
+ scopeRetryCount += 1;
13309
+ let retryResultReceived = false;
13274
13310
  try {
13275
13311
  // #1050 — SETTLE before re-issuing. The panel refuses this race when the
13276
13312
  // graph changed between dispatch and apply, and re-dispatching in the
@@ -13292,6 +13328,7 @@ export function buildPanelToolDefs() {
13292
13328
  // prompt id rather than allowing that stale arm to be synthesized (#2021).
13293
13329
  runDispatchedAt = Date.now();
13294
13330
  res = await ctx.call(runCmd, 20000, observeRunRid);
13331
+ retryResultReceived = true;
13295
13332
  // #1175 — the re-issue can miss its window exactly as the first
13296
13333
  // dispatch can, and this is the one whose outcome is genuinely open
13297
13334
  // (the first was CERTIFIED to have queued nothing). Reconcile it on
@@ -13300,17 +13337,37 @@ export function buildPanelToolDefs() {
13300
13337
  rejection = detectRunRejection(res);
13301
13338
  }
13302
13339
  catch (err) {
13340
+ // A bridge/late-receipt failure after ctx.call already returned must not
13341
+ // erase the retry's own result (notably its retry_of token). Preserve it
13342
+ // verbatim and stop: no further dispatch is justified by a failed local
13343
+ // reconciliation step.
13344
+ if (retryResultReceived) {
13345
+ return appendToolResultText(res, `\n\n(Dispatch history: the retry dispatch returned the result above, but local ` +
13346
+ `late-ack reconciliation failed before this handler could finish ` +
13347
+ `interpreting it. That result is preserved and no further dispatch was ` +
13348
+ `made. Do not infer an additional queue entry from the reconciliation ` +
13349
+ `failure alone. (${err instanceof Error ? err.message : String(err)})`);
13350
+ }
13303
13351
  // ctx.call settles every panel/transport failure into a ToolResult and does
13304
13352
  // not throw today — but this await is the one point where a throw would
13305
- // DESTROY evidence we already hold: that the first dispatch was certified
13306
- // not-queued, and that a second dispatch went out whose outcome is now
13307
- // unknown. Propagating the raw throw would lose both and invite a third
13308
- // dispatch. Report what we observed, claim nothing we did not.
13309
- return fail(`panel_run re-issued the scoped run after the panel's certified run-to-node ` +
13310
- `stamp race, and that SECOND dispatch failed before any reply could be read — ` +
13311
- `its outcome is UNKNOWN and it may have queued a render. The FIRST dispatch was ` +
13312
- `certified by the panel to have queued nothing. Do NOT re-run blindly: check the ` +
13313
- `queue (action:"list") or get_history before deciding. (${err instanceof Error ? err.message : String(err)})`);
13353
+ // DESTROY evidence we already hold: that every prior dispatch was
13354
+ // certified not-queued, and that this dispatch went out whose outcome is
13355
+ // now unknown. Propagating the raw throw would lose both and invite a
13356
+ // blind dispatch. Report what we observed, claim nothing we did not.
13357
+ if (scopeRetryCount === 1) {
13358
+ return fail(`panel_run re-issued the scoped run after the panel's certified run-to-node ` +
13359
+ `stamp race, and that SECOND dispatch failed before any reply could be read ` +
13360
+ `its outcome is UNKNOWN and it may have queued a render. The FIRST dispatch was ` +
13361
+ `certified by the panel to have queued nothing. Do NOT re-run blindly: check the ` +
13362
+ `queue (action:"list") or get_history before deciding. (${err instanceof Error ? err.message : String(err)})`);
13363
+ }
13364
+ return fail(`panel_run re-issued the scoped run ${scopeRetryCount} times after the panel ` +
13365
+ `certified each prior run-to-node stamp race as not-queued, and the ` +
13366
+ `dispatch ${scopeRetryCount + 1} failed before any reply could be read — ` +
13367
+ `its outcome is UNKNOWN and it may have queued a render. The first ` +
13368
+ `${scopeRetryCount} dispatches were certified by the panel to have queued ` +
13369
+ `nothing. Do NOT re-run blindly: check the queue (action:"list") or ` +
13370
+ `get_history before deciding. (${err instanceof Error ? err.message : String(err)})`);
13314
13371
  }
13315
13372
  }
13316
13373
  if (rejection) {
@@ -13327,14 +13384,24 @@ export function buildPanelToolDefs() {
13327
13384
  // cannot tell whether these are one event or two.
13328
13385
  if (!scopeRebuilt)
13329
13386
  return lateAckNote ? appendToolResultText(rejection, lateAckNote) : rejection;
13330
- return appendToolResultText(rejection, `\n\n(Dispatch history: the first graph_run was refused by the panel's run-to-node ` +
13331
- `graph-stamp race, which CERTIFIED that nothing was queued, so the scoped run was ` +
13332
- `re-issued exactly once after a short pause to let pending graph edits land ` +
13333
- `(#1050). The failure above is that SECOND dispatch; it was not retried again. ` +
13334
- `Judge whether anything was queued from that message alone the first dispatch ` +
13335
- `definitely queued nothing. Racing AGAIN after the pause means the graph is still ` +
13336
- `changing under the run: let the canvas settle, then re-run.)` +
13337
- lateAckNote);
13387
+ return appendToolResultText(rejection, scopeRetryCount === 1
13388
+ ? `\n\n(Dispatch history: the first graph_run was refused by the panel's run-to-node ` +
13389
+ `graph-stamp race, which CERTIFIED that nothing was queued, so the scoped run was ` +
13390
+ `re-issued exactly once after a short pause to let pending graph edits land ` +
13391
+ `(#1050). The failure above is that SECOND dispatch; it was not retried again. ` +
13392
+ `Judge whether anything was queued from that message alone the first dispatch ` +
13393
+ `definitely queued nothing. Racing AGAIN after the pause means the graph is still ` +
13394
+ `changing under the run: let the canvas settle, then re-run.)` +
13395
+ lateAckNote
13396
+ : `\n\n(Dispatch history: the first ${scopeRetryCount} graph_run dispatches were each ` +
13397
+ `refused by the panel's run-to-node graph-stamp race, and each CERTIFIED ` +
13398
+ `that nothing was queued. The scoped run was re-issued ${scopeRetryCount} ` +
13399
+ `times after short pauses to let pending graph edits land (#1050). The ` +
13400
+ `failure above is dispatch ${scopeRetryCount + 1}; it was not retried again. ` +
13401
+ `Judge whether anything was queued from that message alone — the first ` +
13402
+ `${scopeRetryCount} dispatches definitely queued nothing. Let the canvas ` +
13403
+ `settle, then re-run.)` +
13404
+ lateAckNote);
13338
13405
  }
13339
13406
  // Attribute this genuine queue to ourselves so a later panel_run in the
13340
13407
  // same batch recognizes the in-flight job as our own and doesn't false-warn
@@ -13465,22 +13532,28 @@ export function buildPanelToolDefs() {
13465
13532
  // clear_pending remedy that would wipe the user's whole batch (#559). A
13466
13533
  // genuinely stalled render is caught by the turn-start stall notice, which
13467
13534
  // does the staleness gating and keeps the interrupt guidance.
13468
- // #772 — never report a dispatch history the caller did not see. When the scoped
13469
- // run only queued on the SECOND dispatch, say so, so the agent does not read the
13470
- // earlier refusal (if it surfaces in a log) as a separate queued job.
13535
+ // #772/#2120 — never report a dispatch history the caller did not see. Say
13536
+ // exactly which earlier dispatches were certified clean, so the agent does
13537
+ // not read a refusal (if it surfaces in a log) as a separate queued job.
13471
13538
  //
13472
13539
  // It says only what was OBSERVED (codex gate). An earlier draft said "exactly one
13473
13540
  // render was queued" — but `batch_count` is "times to queue", so a successful
13474
13541
  // dispatch can enqueue several, and the panel reports one prompt id, not a count.
13475
- // The certified fact is about DISPATCHES, not renders: the first queued nothing,
13476
- // so everything in the queue came from the second. State that and stop.
13477
- const retryNote = scopeRebuilt
13478
- ? "\n\n[NOTE] The first dispatch of this scoped run was refused by the panel's run-to-node " +
13479
- "graph-stamp race (the graph changed between dispatch and apply); the panel certified that " +
13480
- "nothing was queued, so the scope was rebuilt and re-issued once, and THAT is the run above. " +
13481
- "The first dispatch contributed NOTHING to the queue, so whatever this run queued was " +
13482
- "queued once, not twice."
13483
- : "";
13542
+ // The certified fact is about DISPATCHES, not renders: the prior dispatches
13543
+ // queued nothing, so everything in the queue came from the final dispatch.
13544
+ const retryNote = scopeRetryCount === 0
13545
+ ? ""
13546
+ : scopeRetryCount === 1
13547
+ ? "\n\n[NOTE] The first dispatch of this scoped run was refused by the panel's run-to-node " +
13548
+ "graph-stamp race (the graph changed between dispatch and apply); the panel certified that " +
13549
+ "nothing was queued, so the scope was rebuilt and re-issued once, and THAT is the run above. " +
13550
+ "The first dispatch contributed NOTHING to the queue, so whatever this run queued was " +
13551
+ "queued once, not twice."
13552
+ : `\n\n[NOTE] The first ${scopeRetryCount} dispatches of this scoped run were refused by the ` +
13553
+ `panel's run-to-node graph-stamp race (the graph changed between dispatch and apply), ` +
13554
+ `and each was certified to have queued NOTHING. The scope was rebuilt and re-issued ` +
13555
+ `${scopeRetryCount} times, and THAT is the run above. Whatever this run queued came ` +
13556
+ `from the final dispatch; the earlier dispatches contributed NOTHING to the queue.`;
13484
13557
  let warn = "";
13485
13558
  if (pre.connected && pre.running) {
13486
13559
  const pending = Math.max(0, pre.queueDepth - 1);
@@ -15561,6 +15634,10 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
15561
15634
  return fail(surfaceErr +
15562
15635
  " To restart the server without a panel confirmation card, use restart_comfyui.");
15563
15636
  }
15637
+ // #2118: keep the proxy proof alive across the confirmation/preflight
15638
+ // awaits. The proxy is never treated as a ComfyUI process; it only lets
15639
+ // the tab-scoped Manager request be observed against the immutable backend.
15640
+ let proxyRestartTarget;
15564
15641
  // #1819: resolve instance identity BEFORE the confirmation card. Asking the
15565
15642
  // user to confirm a restart, then refusing because we cannot tell which
15566
15643
  // ComfyUI this tab fronts, is two contradictory outcomes (timeout vs refuse)
@@ -15575,10 +15652,14 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
15575
15652
  const identityBound = identityHealthBase != null &&
15576
15653
  sameHttpBase(getComfyUIBaseUrl(), identityHealthBase);
15577
15654
  if (!identityBound) {
15655
+ const candidate = await panelVerifiedProxyRestartTarget();
15656
+ if (proxyTargetBindsPanel(ctx, candidate)) {
15657
+ proxyRestartTarget = candidate;
15658
+ }
15578
15659
  const tabStillHere = typeof ctx.bridge?.canReach === "function"
15579
15660
  ? ctx.bridge.canReach(ctx.tabId) === true
15580
15661
  : true;
15581
- if (tabStillHere) {
15662
+ if (tabStillHere && proxyRestartTarget == null) {
15582
15663
  return restartRefusedPreservingBinding(ctx, unboundLocalRestartRefusalNote(ctx, identityHealthBase, identityBinding.blocker));
15583
15664
  }
15584
15665
  // Tab is gone: confirm will be unreachable and #1671 crash-recovery
@@ -16151,8 +16232,16 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
16151
16232
  // "idle" and a restart aborts whatever is running.
16152
16233
  const configuredRestartCommand = config.comfyuiRestartCommand;
16153
16234
  if (configuredRestartCommand && !isRemoteMode() && !isCloudMode()) {
16154
- const commandHealthBase = captureRebootHealthBase(ctx);
16155
- if (commandHealthBase != null && sameHttpBase(getComfyUIBaseUrl(), commandHealthBase)) {
16235
+ if (proxyRestartTarget != null) {
16236
+ const refreshedProxy = await panelVerifiedProxyRestartTarget();
16237
+ proxyRestartTarget = proxyTargetBindsPanel(ctx, refreshedProxy)
16238
+ ? refreshedProxy
16239
+ : undefined;
16240
+ }
16241
+ const commandHealthBase = proxyRestartTarget?.backendBase ?? captureRebootHealthBase(ctx);
16242
+ const commandBound = proxyRestartTarget != null ||
16243
+ (commandHealthBase != null && sameHttpBase(getComfyUIBaseUrl(), commandHealthBase));
16244
+ if (commandHealthBase != null && commandBound) {
16156
16245
  if (force !== true) {
16157
16246
  let busyCount = null;
16158
16247
  try {
@@ -16191,7 +16280,13 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
16191
16280
  // Re-heal and re-verify at the point of action, exactly as the
16192
16281
  // dispatch path below does.
16193
16282
  ctx.ensureReachable?.();
16194
- const postCheckHealthBase = captureRebootHealthBase(ctx);
16283
+ if (proxyRestartTarget != null) {
16284
+ const postProxy = await panelVerifiedProxyRestartTarget();
16285
+ proxyRestartTarget = proxyTargetBindsPanel(ctx, postProxy)
16286
+ ? postProxy
16287
+ : undefined;
16288
+ }
16289
+ const postCheckHealthBase = proxyRestartTarget?.backendBase ?? captureRebootHealthBase(ctx);
16195
16290
  if (postCheckHealthBase == null ||
16196
16291
  !sameHttpBase(commandHealthBase, postCheckHealthBase)) {
16197
16292
  return ok({
@@ -16271,10 +16366,20 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
16271
16366
  // can be proven. They keep restart_comfyui, and the note says so. Weighed
16272
16367
  // against #814 — where exactly such a user's server was stopped and never came
16273
16368
  // back — declining is the recoverable side.
16369
+ // Re-resolve after the confirmation await. A proxy proof is an endpoint
16370
+ // identity claim, not a durable permission; a retarget or tab rebind makes
16371
+ // it unusable and returns to the existing refusal path.
16372
+ if (proxyRestartTarget != null) {
16373
+ const refreshedProxy = await panelVerifiedProxyRestartTarget();
16374
+ proxyRestartTarget = proxyTargetBindsPanel(ctx, refreshedProxy)
16375
+ ? refreshedProxy
16376
+ : undefined;
16377
+ }
16274
16378
  const preflightBinding = resolveRebootHealthBinding(ctx);
16275
- const preflightHealthBase = preflightBinding.base;
16276
- const preflightBound = preflightHealthBase != null &&
16277
- sameHttpBase(getComfyUIBaseUrl(), preflightHealthBase);
16379
+ const preflightHealthBase = proxyRestartTarget?.backendBase ?? preflightBinding.base;
16380
+ const preflightBound = proxyRestartTarget != null ||
16381
+ (preflightHealthBase != null &&
16382
+ sameHttpBase(getComfyUIBaseUrl(), preflightHealthBase));
16278
16383
  // #848: what the instance was OBSERVED running with, taken from the preflight
16279
16384
  // that already had to resolve it. Nothing new is probed before the dispatch —
16280
16385
  // the no-await invariant between the binding capture and the reboot stands.
@@ -16308,7 +16413,9 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
16308
16413
  // EVERY retarget — any mutation, including a round trip back to the
16309
16414
  // same base, is caught.
16310
16415
  const preflightTargetGeneration = getComfyuiTargetGeneration();
16311
- const preflight = await (localRestartPreflightOverride ?? preflightLocalRestart)();
16416
+ const preflight = proxyRestartTarget
16417
+ ? { ok: true, observedArgv: proxyRestartTarget.argv }
16418
+ : await (localRestartPreflightOverride ?? preflightLocalRestart)();
16312
16419
  // #848: keep the observed launch arguments. Recorded here and spent ONLY on
16313
16420
  // the success path far below, where the reboot has been proven to have
16314
16421
  // cycled THIS instance — it never influences any decision above.
@@ -16327,11 +16434,19 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
16327
16434
  // re-capture BEFORE trusting the result either way. The guarantee
16328
16435
  // that must hold: a stop/reboot is only ever sent when the preflight
16329
16436
  // validated THE instance the dispatch will cycle.
16437
+ if (proxyRestartTarget) {
16438
+ // The proxy branch performs no local stop/preflight. Its direct
16439
+ // backend proof is rechecked immediately before dispatch below.
16440
+ preflightArgvGeneration = proxyRestartTarget.generation;
16441
+ }
16330
16442
  ctx.ensureReachable?.();
16331
16443
  const postPreflightHealthBase = captureRebootHealthBase(ctx);
16332
- const tabFrontsSameInstance = postPreflightHealthBase != null &&
16333
- sameHttpBase(preflightHealthBase, postPreflightHealthBase);
16334
- const configStable = getComfyuiTargetGeneration() === preflightTargetGeneration;
16444
+ const tabFrontsSameInstance = proxyRestartTarget != null ||
16445
+ (postPreflightHealthBase != null &&
16446
+ sameHttpBase(preflightHealthBase, postPreflightHealthBase));
16447
+ const configStable = proxyRestartTarget != null
16448
+ ? getComfyuiTargetGeneration() === proxyRestartTarget.generation
16449
+ : getComfyuiTargetGeneration() === preflightTargetGeneration;
16335
16450
  if (!configStable && tabFrontsSameInstance) {
16336
16451
  // r10: the target config moved MID-CHECK, so the preflight result
16337
16452
  // — pass OR fail — cannot vouch for the tab-fronted instance (a
@@ -16409,7 +16524,17 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
16409
16524
  // this SAME browser tab; a different tab can reuse the same saved-workflow
16410
16525
  // routing id while the original is still reconnecting.
16411
16526
  const preRestartPanelIdentity = ctx.panelConnectionIdentity?.();
16412
- const healthBase = captureRebootHealthBase(ctx);
16527
+ if (proxyRestartTarget != null) {
16528
+ const dispatchProxy = await panelVerifiedProxyRestartTarget();
16529
+ proxyRestartTarget = proxyTargetBindsPanel(ctx, dispatchProxy)
16530
+ ? dispatchProxy
16531
+ : undefined;
16532
+ if (proxyRestartTarget) {
16533
+ preflightArgv = [...proxyRestartTarget.argv];
16534
+ preflightArgvGeneration = proxyRestartTarget.generation;
16535
+ }
16536
+ }
16537
+ const healthBase = proxyRestartTarget?.backendBase ?? captureRebootHealthBase(ctx);
16413
16538
  // THE BINDING RULE APPLIES AT THE DISPATCH POINT, NOT ONLY BEFORE THE AWAIT.
16414
16539
  //
16415
16540
  // The check above happens before the preflight; a tab or connection rebind
@@ -16422,7 +16547,8 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
16422
16547
  //
16423
16548
  // Same rule, same exclusions: only a LOCAL target we cannot tie to the
16424
16549
  // instance this server accounts for is refused.
16425
- const dispatchBound = healthBase != null && sameHttpBase(getComfyUIBaseUrl(), healthBase);
16550
+ const dispatchBound = proxyRestartTarget != null ||
16551
+ (healthBase != null && sameHttpBase(getComfyUIBaseUrl(), healthBase));
16426
16552
  if (!dispatchBound && !isRemoteMode() && !isCloudMode()) {
16427
16553
  return restartRefusedPreservingBinding(ctx, "Refusing to restart ComfyUI: the panel connection changed while the restart " +
16428
16554
  "was being prepared, and I can no longer confirm which ComfyUI this tab " +
@@ -16614,6 +16740,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
16614
16740
  // server replaced at the same URL between the reboot and here. That gap is real
16615
16741
  // and tracked in #871; this gate narrows the window, it does not close it.
16616
16742
  healthBase != null &&
16743
+ proxyRestartTarget == null &&
16617
16744
  sameHttpBase(getComfyUIBaseUrl(), healthBase)) {
16618
16745
  return runHeadlessManagedRestart({
16619
16746
  healthBase,