comfyui-mcp 0.52.42 → 0.52.44

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.
@@ -30,8 +30,11 @@ import { panelRecoveryContext } from "../services/panel-recovery.js";
30
30
  import { isPanelAutoInstallDisabled } from "../services/panel-installer.js";
31
31
  import { releaseOwnedPanelLock } from "../services/panel-pin-guard.js";
32
32
  import { SelfRestarter, canSelfRestart } from "../services/self-restart.js";
33
+ import { tickPanelAutoUpdate } from "../services/panel-auto-update.js";
33
34
  import { pairUrlDurability } from "./pair-durability.js";
34
35
  import { loadOrCreatePairToken } from "./pair-token-store.js";
36
+ import { autoUpdateApplyAllowed, pairAutoUpdateDisclosure, pairingActiveOf, pairingTransportOf, } from "../services/auto-update-gate.js";
37
+ import { loadPairUpdatePrefs, savePairUpdatePrefs } from "./pair-update-prefs.js";
35
38
  import { SessionStore, workflowIdentityParts, carryWorkflowCommandStamp } from "./session-store.js";
36
39
  import { unreachableReason, noPanelTabReason, identityReason } from "./fence-refusal.js";
37
40
  import { SHARED_SESSION_SCOPE, isScopeAddress, conversationTabs, shouldRetireSharedAgent, messageOrigin, normalizeHelloBackend, workflowOriginNote, } from "../services/session-scope.js";
@@ -81,7 +84,7 @@ import { PanelAgentManager, fetchSupportedModels, fetchSupportedCommands, isEffo
81
84
  import { promptText } from "./error-text.js";
82
85
  import { callToolAdmission } from "./call-tool-admission.js";
83
86
  import { DEFERRED_PANEL_TOOLS_STEERING, withDeferredPanelToolsNote, } from "../deferred-panel-tools.js";
84
- import { createPanelMcpServer, makePanelToolCtx, resolvePinTarget, secretSavedReply, setApplyMcpReload, RETRY_TOKEN_CMDS, } from "./panel-tools.js";
87
+ import { createPanelMcpServer, makePanelToolCtx, resolvePinTarget, secretSavedReply, setApplyMcpReload, forgetAbandonedConfirmCards, RETRY_TOKEN_CMDS, } from "./panel-tools.js";
85
88
  import { optionsAckFrame, optionsErrorAckFrame, optionsRequestMeta, } from "./options-ack.js";
86
89
  import { readUserMcpServers } from "../services/user-mcp-config.js";
87
90
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -124,7 +127,7 @@ import { startPanelConsoleHttpServer } from "./panel-console-http.js";
124
127
  import { readComfyuiCrashLog, formatCrashNote } from "../services/crash-log.js";
125
128
  import { QueueMonitor } from "../services/queue-monitor.js";
126
129
  import { RunCompletions, describe as describeCorrelation, } from "./run-completion-journal.js";
127
- import { createRunCompletionWatchdog, resolveHistoryCompletionImages } from "./run-completion-watchdog.js";
130
+ import { createRunCompletionWatchdog, resolveHistoryCompletionImages, resolveHistoryCompletionStatus, } from "./run-completion-watchdog.js";
128
131
  import { AskAnswers, preview as previewQuestion } from "./ask-answer-journal.js";
129
132
  import { initRunpodWatcher, getRunpodWatcher } from "../services/runpod-watch.js";
130
133
  import { getPod } from "../services/runpod-client.js";
@@ -1054,20 +1057,25 @@ export async function runPanelOrchestrator() {
1054
1057
  let pairToken = envPairToken;
1055
1058
  let pairListenerStarted = false;
1056
1059
  let pairTunnel = null;
1060
+ // Forward declaration — assigned after teardownCore exists. Declared here so
1061
+ // pair / apply_updates_now handlers can refresh the restarter without hitting
1062
+ // the TDZ if a frame arrives before that assignment.
1063
+ let selfRestarter = null;
1057
1064
  /**
1058
- * #875is a phone paired over a TUNNEL right now?
1065
+ * #1963APPLY gate for auto-update. Checking still runs.
1059
1066
  *
1060
- * Gates the self-restarter (see its allIdle below). A tunnel URL cannot survive
1061
- * a restart: cloudflared mints a fresh quick-tunnel hostname per run and there
1062
- * is no way to pin it, so restarting under a live tunnel breaks a phone that is
1063
- * connected at that moment. A LAN URL is fine the token persists now — which
1064
- * is why this is narrowed to the tunnel rather than to pairing in general.
1067
+ * pairingActive is STICKY on `pairTunnel` (the handle exists from mint until
1068
+ * process exit). A live-socket test is the hole a locked-screen phone falls
1069
+ * through: the OS has closed the socket, the gate opened, the hostname
1070
+ * rotated. The pair-time toggle (default ON) is how a desk session applies
1071
+ * anyway; apply_updates_now is the one-shot override.
1065
1072
  */
1066
- // Requires BOTH: a tunnel exists AND a client is connected through it right
1067
- // now. `pairTunnel` alone is not liveness — nothing stops the tunnel until the
1068
- // process exits, so gating on it would postpone every future update after a
1069
- // single pairing, rather than deferring to the next disconnect as intended.
1070
- const tunnelPairingLive = () => pairTunnel !== null && bridge.hasLiveHeadlessClient();
1073
+ const autoUpdateGateInput = () => ({
1074
+ activeTransport: pairingTransportOf(pairTunnel),
1075
+ pairingActive: pairingActiveOf(pairTunnel),
1076
+ deferWhilePaired: loadPairUpdatePrefs().deferWhilePaired,
1077
+ });
1078
+ const applyAutoUpdateAllowed = () => autoUpdateApplyAllowed(autoUpdateGateInput());
1071
1079
  // #875 — the token is PERSISTED, not per-session. It used to be minted fresh on
1072
1080
  // every run, so a self-restart (on by default, hourly npm check) invalidated the
1073
1081
  // URL the phone had saved. The reporter experienced that as "updating the npm
@@ -2608,7 +2616,14 @@ export async function runPanelOrchestrator() {
2608
2616
  // newcomer never asked the previous occupant's questions, so its answers must
2609
2617
  // stop being recoverable and stop being pushed. closeAsks downgrades and
2610
2618
  // unsends; it never deletes, so those answers are still reported.
2611
- bridge.setTabTakenOverListener((tabId) => AskAnswers.closeAsks(tabId));
2619
+ // panel#1554 — same boundary, same reason: a confirmation card the previous
2620
+ // occupant abandoned must not be claimable by the newcomer. Both retirements ride
2621
+ // THIS one listener because the bridge takes a single takeover listener, so a
2622
+ // second setTabTakenOverListener call would silently replace the first.
2623
+ bridge.setTabTakenOverListener((tabId) => {
2624
+ AskAnswers.closeAsks(tabId);
2625
+ forgetAbandonedConfirmCards(tabId);
2626
+ });
2612
2627
  bridge.setTabGoneListener((tabId, incarnation) => AskAnswers.retireDebt(tabId, incarnation));
2613
2628
  // #694 — the bridge retains a late mutation only for commands that can come
2614
2629
  // back as a retry token, which is the retry-token layer's own set. Installed
@@ -2914,6 +2929,12 @@ export async function runPanelOrchestrator() {
2914
2929
  backendForTab,
2915
2930
  backendOfKey: backendOf,
2916
2931
  info: (msg) => logger.info(msg),
2932
+ // panel#1557 — a unique-canvas reconnect that hello'd without `backend`
2933
+ // joined the default conversation; claiming puts it on this one so the
2934
+ // pin we just wrote is not immediately invalidated at resolution.
2935
+ claimTab: (tab, backend) => {
2936
+ tabBackends.set(tab, backend);
2937
+ },
2917
2938
  }));
2918
2939
  // #884 P1 (confirming gate 2) — a hello whose `backend` is absent or unknown
2919
2940
  // JOINS the default conversation, so its backend-qualified buffers must drain
@@ -3504,6 +3525,10 @@ export async function runPanelOrchestrator() {
3504
3525
  logger.warn(`[panel-orchestrator] readiness probe failed: ${err instanceof Error ? err.message : String(err)}`);
3505
3526
  }
3506
3527
  }
3528
+ // Assigned next to the queue-status timer below. Declared here so the hello
3529
+ // path can reconcile owed panel_run tickets the moment the panel reconnects
3530
+ // (#1556) without waiting for that timer to construct it.
3531
+ let runCompletionWatchdog;
3507
3532
  bridge.onPanelMessage = (event) => {
3508
3533
  // Connect ack: the instant a panel tab connects, the orchestrator announces
3509
3534
  // itself so "connected" means "a real agent is attending" — not merely "a
@@ -3958,6 +3983,12 @@ export async function runPanelOrchestrator() {
3958
3983
  // are change-only, so a tab connecting MID-render would otherwise wait for
3959
3984
  // the next state transition to learn a job is already running.
3960
3985
  bridge.push(buildQueueStatusFrame(QueueMonitor.snapshot()), panelTab);
3986
+ // #1556 — a hello is a reconnect (or the first chance after a missed
3987
+ // ComfyUI WS frame). Look up every still-owed panel_run in /history NOW
3988
+ // and journal a completion if ComfyUI already finished it, instead of
3989
+ // waiting the synthesis grace. Exactly-once: the journal skips a run it
3990
+ // already holds.
3991
+ void runCompletionWatchdog?.reconcile(RunCompletions.owedCompletions().map((t) => t.promptId));
3961
3992
  // Seed the RunPod control panel too — a tab that just connected gets the
3962
3993
  // current pod-status frame (or a cleared one when nothing is watched).
3963
3994
  const rpFrame = getRunpodWatcher()?.current();
@@ -4538,6 +4569,18 @@ export async function runPanelOrchestrator() {
4538
4569
  if (event.type === "pair" && event.tab_id) {
4539
4570
  const mode = event.mode === "tunnel" ? "tunnel" : "lan";
4540
4571
  const tabId = event.tab_id;
4572
+ // Pair-time toggle, default ON. An explicit boolean is the only thing we
4573
+ // persist — a missing field leaves the last saved (or default) value.
4574
+ const rawDefer = event.defer_while_paired;
4575
+ if (typeof rawDefer === "boolean") {
4576
+ try {
4577
+ savePairUpdatePrefs({ deferWhilePaired: rawDefer });
4578
+ }
4579
+ catch (err) {
4580
+ logger.warn(`[panel-orchestrator] could not persist pair-update prefs: ${err instanceof Error ? err.message : String(err)}`);
4581
+ }
4582
+ selfRestarter?.requestApplyPolicyRefresh();
4583
+ }
4541
4584
  void (async () => {
4542
4585
  const token = await ensurePairListener();
4543
4586
  let url;
@@ -4582,12 +4625,55 @@ export async function runPanelOrchestrator() {
4582
4625
  (durability.survivesRestart
4583
4626
  ? "survives restart"
4584
4627
  : `rotates on restart: ${durability.rotates.join(", ")}`));
4585
- bridge.push({ type: "pair_url", mode, url, durability }, tabId);
4628
+ const auto_update = pairAutoUpdateDisclosure(autoUpdateGateInput());
4629
+ bridge.push({ type: "pair_url", mode, url, durability, auto_update }, tabId);
4586
4630
  })().catch((err) => {
4587
4631
  bridge.push({ type: "pair_error", mode, error: err instanceof Error ? err.message : String(err) }, tabId);
4588
4632
  });
4589
4633
  return;
4590
4634
  }
4635
+ // #1963 — flip the pair-time "Don't update while my phone is paired" toggle
4636
+ // after the URL is already minted (the checkbox on the QR modal).
4637
+ if (event.type === "set_pair_update_pref" && event.tab_id) {
4638
+ const tabId = event.tab_id;
4639
+ const rawDefer = event.defer_while_paired;
4640
+ if (typeof rawDefer !== "boolean") {
4641
+ bridge.push({ type: "pair_update_pref", ok: false, error: "defer_while_paired must be a boolean" }, tabId);
4642
+ return;
4643
+ }
4644
+ try {
4645
+ const prefs = savePairUpdatePrefs({ deferWhilePaired: rawDefer });
4646
+ bridge.push({
4647
+ type: "pair_update_pref",
4648
+ ok: true,
4649
+ defer_while_paired: prefs.deferWhilePaired,
4650
+ apply: autoUpdateApplyAllowed({
4651
+ ...autoUpdateGateInput(),
4652
+ deferWhilePaired: prefs.deferWhilePaired,
4653
+ }),
4654
+ }, tabId);
4655
+ selfRestarter?.requestApplyPolicyRefresh();
4656
+ }
4657
+ catch (err) {
4658
+ bridge.push({
4659
+ type: "pair_update_pref",
4660
+ ok: false,
4661
+ error: err instanceof Error ? err.message : String(err),
4662
+ }, tabId);
4663
+ }
4664
+ return;
4665
+ }
4666
+ // #1963 — "Update now" from the desk (or from the deferred-update notice).
4667
+ // One-shot: ignores the tunnel gate for this tick only.
4668
+ if (event.type === "apply_updates_now") {
4669
+ const tabId = event.tab_id;
4670
+ logger.info("[panel-orchestrator] apply_updates_now — checking and applying updates (bypass tunnel gate)");
4671
+ void selfRestarter?.updateTick({ forceApply: true });
4672
+ if (tabId) {
4673
+ bridge.push({ type: "ack", ok: true, kind: "apply_updates_now" }, tabId);
4674
+ }
4675
+ return;
4676
+ }
4591
4677
  // Direct tool channel: the mobile app invokes a WHITELISTED read/download tool
4592
4678
  // (structured data for nav lists + rig downloads) without an agent turn. Replies
4593
4679
  // with a `tool_result` frame the client correlates by rid.
@@ -5899,9 +5985,10 @@ export async function runPanelOrchestrator() {
5899
5985
  // after the grace is synthesised into the SAME journal the real frame uses.
5900
5986
  // See run-completion-watchdog.ts for why it waits, and why the panel's frame
5901
5987
  // still wins whenever it is coming.
5902
- const runCompletionWatchdog = createRunCompletionWatchdog({
5988
+ const wd = createRunCompletionWatchdog({
5903
5989
  awaiting: (promptId) => RunCompletions.awaitingCompletion(promptId),
5904
5990
  resolveOutputs: (promptId) => resolveHistoryCompletionImages(promptId),
5991
+ lookupStatus: (promptId) => resolveHistoryCompletionStatus(promptId),
5905
5992
  deliver: (payload, ticket) => {
5906
5993
  // The SAME arrival path the panel's frame takes: correlated once, here,
5907
5994
  // against the ticket that is still open — so the agent is told this is the
@@ -5914,13 +6001,14 @@ export async function runPanelOrchestrator() {
5914
6001
  flushRunCompletions(ticket.tabId);
5915
6002
  },
5916
6003
  });
6004
+ runCompletionWatchdog = wd;
5917
6005
  const queueStatusBroadcaster = createQueueStatusBroadcaster(() => QueueMonitor.snapshot(), (frame) => void bridge.push(frame),
5918
6006
  // TEE, not a second drain: drainCompletions() splices, so the watchdog has
5919
6007
  // to read the same array the broadcaster is handed rather than call it
5920
6008
  // again — a second call would race and each consumer would see only half.
5921
6009
  () => {
5922
6010
  const completions = QueueMonitor.drainCompletions();
5923
- runCompletionWatchdog.observe(completions);
6011
+ wd.observe(completions);
5924
6012
  return completions;
5925
6013
  });
5926
6014
  // Each tick first refreshes the monitor over HTTP (GET /queue + /history
@@ -5934,7 +6022,7 @@ export async function runPanelOrchestrator() {
5934
6022
  // …and only THEN expire the watchdog's arms: the broadcaster tick is what
5935
6023
  // feeds it (the drain tee above), so ticking it first would let an
5936
6024
  // observation sit a whole extra second before it is even armed. #1789.
5937
- runCompletionWatchdog.tick();
6025
+ wd.tick();
5938
6026
  });
5939
6027
  }, 1000);
5940
6028
  queueStatusTimer.unref?.();
@@ -6205,9 +6293,6 @@ export async function runPanelOrchestrator() {
6205
6293
  : " — remote target: installs/downloads run ON the ComfyUI host via its Manager (a local path would be the wrong filesystem; only local-FS tools like node_pack (action:'verify') are unavailable)";
6206
6294
  logger.info(`[panel-orchestrator] ready — bridge on ws://127.0.0.1:${bridgePort}, console on ${consoleUrl}; an agent spawns per ComfyUI tab on its first message (model=${model}, comfyui=${comfyuiUrl}${pathNote})`);
6207
6295
  let shuttingDown = false;
6208
- // Forward declaration — assigned right after teardownCore exists; teardownCore
6209
- // stops its timers so no update-check tick can fire mid-teardown.
6210
- let selfRestarter = null;
6211
6296
  /** Everything shutdown does EXCEPT exiting — shared with the self-restarter,
6212
6297
  * which spawns its replacement first and exits itself after this settles. */
6213
6298
  const teardownCore = async () => {
@@ -6334,24 +6419,12 @@ export async function runPanelOrchestrator() {
6334
6419
  // …and the same for an ask answer the user actually gave that has not
6335
6420
  // reached the agent yet (#486). Restarting would destroy it silently.
6336
6421
  !AskAnswers.hasOutstanding() &&
6337
- !QueueMonitor.isBusy() &&
6338
- // #875a LIVE TUNNEL PAIRING SESSION defers the restart to the next
6339
- // disconnect. The token now persists, so a LAN URL survives a restart; a
6340
- // tunnel URL cannot, because cloudflared mints a fresh quick-tunnel
6341
- // hostname every run and there is no way to pin it. Restarting under a live
6342
- // tunnel therefore breaks a phone that is connected RIGHT NOW, to install
6343
- // an update nobody asked for at that moment — which is what the reporter
6344
- // experienced and (reasonably) blamed on the npm version.
6345
- //
6346
- // Deferral, not cancellation: the update check still runs and the restart
6347
- // stays armed, so it fires as soon as the tunnel goes away. The cost is
6348
- // that a tunnel left up indefinitely postpones updates indefinitely, so it
6349
- // is DISCLOSED — in pair-durability's tunnel note, which the panel shows at
6350
- // pair time. Deliberately not announced from here: this is a predicate the
6351
- // restarter polls, and emitting from it would either fire repeatedly or
6352
- // need its own state to suppress itself. Pair time is also where the user
6353
- // is actually looking (#1077: an orchestrator log may be unreachable).
6354
- !tunnelPairingLive(),
6422
+ !QueueMonitor.isBusy(),
6423
+ // #1963transport gate is applyAllowed, not allIdle. Idle is in-flight
6424
+ // work; this is "would a restart rotate the phone's tunnel hostname?"
6425
+ // Checking still runs; APPLY (disk + restart, and the panel pack) does not.
6426
+ applyAllowed: applyAutoUpdateAllowed,
6427
+ panelTick: async ({ apply }) => tickPanelAutoUpdate({ apply }),
6355
6428
  announce: (text) => void bridge.push({ type: "say", text }),
6356
6429
  teardown: teardownCore,
6357
6430
  });