comfyui-mcp 0.52.43 → 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";
@@ -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
@@ -4554,6 +4569,18 @@ export async function runPanelOrchestrator() {
4554
4569
  if (event.type === "pair" && event.tab_id) {
4555
4570
  const mode = event.mode === "tunnel" ? "tunnel" : "lan";
4556
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
+ }
4557
4584
  void (async () => {
4558
4585
  const token = await ensurePairListener();
4559
4586
  let url;
@@ -4598,12 +4625,55 @@ export async function runPanelOrchestrator() {
4598
4625
  (durability.survivesRestart
4599
4626
  ? "survives restart"
4600
4627
  : `rotates on restart: ${durability.rotates.join(", ")}`));
4601
- 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);
4602
4630
  })().catch((err) => {
4603
4631
  bridge.push({ type: "pair_error", mode, error: err instanceof Error ? err.message : String(err) }, tabId);
4604
4632
  });
4605
4633
  return;
4606
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
+ }
4607
4677
  // Direct tool channel: the mobile app invokes a WHITELISTED read/download tool
4608
4678
  // (structured data for nav lists + rig downloads) without an agent turn. Replies
4609
4679
  // with a `tool_result` frame the client correlates by rid.
@@ -6223,9 +6293,6 @@ export async function runPanelOrchestrator() {
6223
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)";
6224
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})`);
6225
6295
  let shuttingDown = false;
6226
- // Forward declaration — assigned right after teardownCore exists; teardownCore
6227
- // stops its timers so no update-check tick can fire mid-teardown.
6228
- let selfRestarter = null;
6229
6296
  /** Everything shutdown does EXCEPT exiting — shared with the self-restarter,
6230
6297
  * which spawns its replacement first and exits itself after this settles. */
6231
6298
  const teardownCore = async () => {
@@ -6352,24 +6419,12 @@ export async function runPanelOrchestrator() {
6352
6419
  // …and the same for an ask answer the user actually gave that has not
6353
6420
  // reached the agent yet (#486). Restarting would destroy it silently.
6354
6421
  !AskAnswers.hasOutstanding() &&
6355
- !QueueMonitor.isBusy() &&
6356
- // #875a LIVE TUNNEL PAIRING SESSION defers the restart to the next
6357
- // disconnect. The token now persists, so a LAN URL survives a restart; a
6358
- // tunnel URL cannot, because cloudflared mints a fresh quick-tunnel
6359
- // hostname every run and there is no way to pin it. Restarting under a live
6360
- // tunnel therefore breaks a phone that is connected RIGHT NOW, to install
6361
- // an update nobody asked for at that moment — which is what the reporter
6362
- // experienced and (reasonably) blamed on the npm version.
6363
- //
6364
- // Deferral, not cancellation: the update check still runs and the restart
6365
- // stays armed, so it fires as soon as the tunnel goes away. The cost is
6366
- // that a tunnel left up indefinitely postpones updates indefinitely, so it
6367
- // is DISCLOSED — in pair-durability's tunnel note, which the panel shows at
6368
- // pair time. Deliberately not announced from here: this is a predicate the
6369
- // restarter polls, and emitting from it would either fire repeatedly or
6370
- // need its own state to suppress itself. Pair time is also where the user
6371
- // is actually looking (#1077: an orchestrator log may be unreachable).
6372
- !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 }),
6373
6428
  announce: (text) => void bridge.push({ type: "say", text }),
6374
6429
  teardown: teardownCore,
6375
6430
  });