comfyui-mcp 0.48.21 → 0.48.23
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.
- package/README.md +2 -2
- package/dist/comfyui/client.js +46 -6
- package/dist/comfyui/client.js.map +1 -1
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/orchestrator/error-text.js +29 -1
- package/dist/orchestrator/error-text.js.map +1 -1
- package/dist/orchestrator/error-text.test.js +28 -0
- package/dist/orchestrator/error-text.test.js.map +1 -1
- package/dist/orchestrator/index.js +331 -56
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +122 -31
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +1256 -198
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/orchestrator/ready-banner.js +70 -0
- package/dist/orchestrator/ready-banner.js.map +1 -0
- package/dist/orchestrator/session-store.js +163 -28
- package/dist/orchestrator/session-store.js.map +1 -1
- package/dist/services/api-nodes.js +5 -7
- package/dist/services/api-nodes.js.map +1 -1
- package/dist/services/download-cache.js +126 -18
- package/dist/services/download-cache.js.map +1 -1
- package/dist/services/download-jobs.js +463 -14
- package/dist/services/download-jobs.js.map +1 -1
- package/dist/services/download-progress.js +252 -3
- package/dist/services/download-progress.js.map +1 -1
- package/dist/services/env-capabilities.js +80 -9
- package/dist/services/env-capabilities.js.map +1 -1
- package/dist/services/generate-audio.js +16 -0
- package/dist/services/generate-audio.js.map +1 -1
- package/dist/services/local-models-fallback.js +7 -3
- package/dist/services/local-models-fallback.js.map +1 -1
- package/dist/services/manifest.js +180 -65
- package/dist/services/manifest.js.map +1 -1
- package/dist/services/model-resolver.js +267 -11
- package/dist/services/model-resolver.js.map +1 -1
- package/dist/services/node-management.js +179 -45
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/output-dir.js +33 -3
- package/dist/services/output-dir.js.map +1 -1
- package/dist/services/panel-secrets.js +19 -7
- package/dist/services/panel-secrets.js.map +1 -1
- package/dist/services/process-control.js +107 -8
- package/dist/services/process-control.js.map +1 -1
- package/dist/services/queue-monitor.js +45 -2
- package/dist/services/queue-monitor.js.map +1 -1
- package/dist/services/queue-monitor.stall.test.js +78 -0
- package/dist/services/queue-monitor.stall.test.js.map +1 -0
- package/dist/services/secure-bridge.js +84 -0
- package/dist/services/secure-bridge.js.map +1 -1
- package/dist/services/storage/azure-blob.js +8 -3
- package/dist/services/storage/azure-blob.js.map +1 -1
- package/dist/services/storage/index.js +3 -3
- package/dist/services/storage/index.js.map +1 -1
- package/dist/services/storage/s3.js +9 -3
- package/dist/services/storage/s3.js.map +1 -1
- package/dist/services/ui-bridge.js +341 -24
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/services/workflow-converter.js +767 -72
- package/dist/services/workflow-converter.js.map +1 -1
- package/dist/services/workflow-deps.js +20 -1
- package/dist/services/workflow-deps.js.map +1 -1
- package/dist/services/workflow-lock.js +127 -27
- package/dist/services/workflow-lock.js.map +1 -1
- package/dist/services/workflow-target-store.js +7 -3
- package/dist/services/workflow-target-store.js.map +1 -1
- package/dist/services/workspace-env.js +48 -0
- package/dist/services/workspace-env.js.map +1 -1
- package/dist/tools/diagnostics.js +10 -1
- package/dist/tools/diagnostics.js.map +1 -1
- package/dist/tools/generate-audio.js +44 -0
- package/dist/tools/generate-audio.js.map +1 -1
- package/dist/tools/model-explorer.js +85 -5
- package/dist/tools/model-explorer.js.map +1 -1
- package/dist/tools/model-extras.js +24 -4
- package/dist/tools/model-extras.js.map +1 -1
- package/dist/tools/model-management.js +124 -14
- package/dist/tools/model-management.js.map +1 -1
- package/dist/tools/report-issue.js +220 -66
- package/dist/tools/report-issue.js.map +1 -1
- package/dist/tools/report-issue.test.js +223 -83
- package/dist/tools/report-issue.test.js.map +1 -1
- package/dist/tools/skills-access.js +7 -2
- package/dist/tools/skills-access.js.map +1 -1
- package/dist/tools/template-schema.js +5 -5
- package/dist/tools/template-schema.js.map +1 -1
- package/dist/tools/workflow-compose.js +18 -3
- package/dist/tools/workflow-compose.js.map +1 -1
- package/dist/tools/workflow-visualize.js +1 -1
- package/dist/tools/workflow-visualize.js.map +1 -1
- package/package.json +1 -1
- package/packs/anima/install-runpod.sh +0 -1
- package/packs/anima/install-windows.bat +0 -1
- package/packs/anima/manifest.yaml +0 -1
- package/packs/anima-img2img/install-runpod.sh +0 -1
- package/packs/anima-img2img/install-windows.bat +0 -1
- package/packs/anima-img2img/manifest.yaml +0 -1
- package/packs/anima-inpaint/install-runpod.sh +0 -1
- package/packs/anima-inpaint/install-windows.bat +0 -1
- package/packs/anima-inpaint/manifest.yaml +0 -1
- package/packs/anima-txt2img/install-runpod.sh +0 -1
- package/packs/anima-txt2img/install-windows.bat +0 -1
- package/packs/anima-txt2img/manifest.yaml +0 -1
- package/packs/artokun-flow/install-runpod.sh +0 -1
- package/packs/artokun-flow/install-windows.bat +0 -1
- package/packs/artokun-flow/manifest.yaml +0 -1
- package/packs/ernie/install-runpod.sh +0 -1
- package/packs/ernie/install-windows.bat +0 -1
- package/packs/ernie/manifest.yaml +0 -1
- package/packs/ernie-combo/install-runpod.sh +0 -1
- package/packs/ernie-combo/install-windows.bat +0 -1
- package/packs/ernie-combo/manifest.yaml +0 -1
- package/packs/ernie-img2img/install-runpod.sh +0 -1
- package/packs/ernie-img2img/install-windows.bat +0 -1
- package/packs/ernie-img2img/manifest.yaml +0 -1
- package/packs/ernie-txt2img/install-runpod.sh +0 -1
- package/packs/ernie-txt2img/install-windows.bat +0 -1
- package/packs/ernie-txt2img/manifest.yaml +0 -1
- package/packs/ideogram/install-runpod.sh +0 -1
- package/packs/ideogram/install-windows.bat +0 -1
- package/packs/ideogram/manifest.yaml +0 -1
- package/packs/ideogram-img2img/install-runpod.sh +0 -1
- package/packs/ideogram-img2img/install-windows.bat +0 -1
- package/packs/ideogram-img2img/manifest.yaml +0 -1
- package/packs/ideogram-txt2img/install-runpod.sh +0 -1
- package/packs/ideogram-txt2img/install-windows.bat +0 -1
- package/packs/ideogram-txt2img/manifest.yaml +0 -1
- package/packs/ltx-2.3/install-runpod.sh +0 -1
- package/packs/ltx-2.3/install-windows.bat +0 -1
- package/packs/ltx-2.3/manifest.yaml +0 -1
- package/packs/ltx-2.3-extender/install-runpod.sh +0 -1
- package/packs/ltx-2.3-extender/install-windows.bat +0 -1
- package/packs/ltx-2.3-extender/manifest.yaml +0 -1
- package/packs/ltx-2.3-extender-no-audio/install-runpod.sh +0 -1
- package/packs/ltx-2.3-extender-no-audio/install-windows.bat +0 -1
- package/packs/ltx-2.3-extender-no-audio/manifest.yaml +0 -1
- package/packs/ltx-2.3-flf/install-runpod.sh +0 -1
- package/packs/ltx-2.3-flf/install-windows.bat +0 -1
- package/packs/ltx-2.3-flf/manifest.yaml +0 -1
- package/packs/ltx-2.3-img2vid/install-runpod.sh +0 -1
- package/packs/ltx-2.3-img2vid/install-windows.bat +0 -1
- package/packs/ltx-2.3-img2vid/manifest.yaml +3 -4
- package/packs/ltx-2.3-txt2vid/install-runpod.sh +0 -1
- package/packs/ltx-2.3-txt2vid/install-windows.bat +0 -1
- package/packs/ltx-2.3-txt2vid/manifest.yaml +3 -4
- package/packs/ltx-2.3-xy-plot/install-runpod.sh +0 -1
- package/packs/ltx-2.3-xy-plot/install-windows.bat +0 -1
- package/packs/ltx-2.3-xy-plot/manifest.yaml +0 -1
- package/packs/ltx23-distill-3stage/install-runpod.sh +0 -1
- package/packs/ltx23-distill-3stage/install-windows.bat +0 -1
- package/packs/ltx23-distill-3stage/manifest.yaml +0 -1
- package/packs/qwen-image/install-runpod.sh +0 -1
- package/packs/qwen-image/install-windows.bat +0 -1
- package/packs/qwen-image/manifest.yaml +0 -1
- package/packs/qwen-image-edit/install-runpod.sh +0 -1
- package/packs/qwen-image-edit/install-windows.bat +0 -1
- package/packs/qwen-image-edit/manifest.yaml +0 -1
- package/packs/qwen-image-edit-edit/install-runpod.sh +0 -1
- package/packs/qwen-image-edit-edit/install-windows.bat +0 -1
- package/packs/qwen-image-edit-edit/manifest.yaml +0 -1
- package/packs/wan-animate/install-runpod.sh +0 -1
- package/packs/wan-animate/install-windows.bat +0 -1
- package/packs/wan-animate/manifest.yaml +0 -1
- package/packs/wan-animate-character/install-runpod.sh +0 -1
- package/packs/wan-animate-character/install-windows.bat +0 -1
- package/packs/wan-animate-character/manifest.yaml +0 -1
- package/packs/wan-animate-ofm/install-runpod.sh +0 -1
- package/packs/wan-animate-ofm/install-windows.bat +0 -1
- package/packs/wan-animate-ofm/manifest.yaml +0 -1
- package/packs/wan-longer-videos/install-runpod.sh +0 -1
- package/packs/wan-longer-videos/install-windows.bat +0 -1
- package/packs/wan-longer-videos/manifest.yaml +0 -1
- package/packs/wan-longer-videos-i2v/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-i2v/install-windows.bat +0 -1
- package/packs/wan-longer-videos-i2v/manifest.yaml +0 -1
- package/packs/wan-longer-videos-i2v-96gb/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-i2v-96gb/install-windows.bat +0 -1
- package/packs/wan-longer-videos-i2v-96gb/manifest.yaml +0 -1
- package/packs/wan-longer-videos-t2v/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-t2v/install-windows.bat +0 -1
- package/packs/wan-longer-videos-t2v/manifest.yaml +0 -1
- package/packs/wan-longer-videos-t2v-96gb/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-t2v-96gb/install-windows.bat +0 -1
- package/packs/wan-longer-videos-t2v-96gb/manifest.yaml +0 -1
- package/packs/wan-longer-videos-v2v/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-v2v/install-windows.bat +0 -1
- package/packs/wan-longer-videos-v2v/manifest.yaml +0 -1
- package/packs/wan-longer-videos-v2v-96gb/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-v2v-96gb/install-windows.bat +0 -1
- package/packs/wan-longer-videos-v2v-96gb/manifest.yaml +0 -1
- package/packs/wan-multitalk/install-runpod.sh +0 -1
- package/packs/wan-multitalk/install-windows.bat +0 -1
- package/packs/wan-multitalk/manifest.yaml +0 -1
- package/packs/wan-pusa-extend/install-runpod.sh +0 -1
- package/packs/wan-pusa-extend/install-windows.bat +0 -1
- package/packs/wan-pusa-extend/manifest.yaml +0 -1
- package/packs/wan-transparent/install-runpod.sh +0 -1
- package/packs/wan-transparent/install-windows.bat +0 -1
- package/packs/wan-transparent/manifest.yaml +0 -1
- package/packs/wan-transparent-img2vid/install-runpod.sh +0 -1
- package/packs/wan-transparent-img2vid/install-windows.bat +0 -1
- package/packs/wan-transparent-img2vid/manifest.yaml +0 -1
- package/packs/wan-transparent-img2vid-96gb/install-runpod.sh +0 -1
- package/packs/wan-transparent-img2vid-96gb/install-windows.bat +0 -1
- package/packs/wan-transparent-img2vid-96gb/manifest.yaml +0 -1
- package/packs/z-image-base/install-runpod.sh +0 -1
- package/packs/z-image-base/install-windows.bat +0 -1
- package/packs/z-image-base/manifest.yaml +0 -1
- package/packs/z-image-base-combo/install-runpod.sh +0 -1
- package/packs/z-image-base-combo/install-windows.bat +0 -1
- package/packs/z-image-base-combo/manifest.yaml +0 -1
- package/packs/z-image-base-controlnet/install-runpod.sh +0 -1
- package/packs/z-image-base-controlnet/install-windows.bat +0 -1
- package/packs/z-image-base-controlnet/manifest.yaml +0 -1
- package/packs/z-image-base-img2img/install-runpod.sh +0 -1
- package/packs/z-image-base-img2img/install-windows.bat +0 -1
- package/packs/z-image-base-img2img/manifest.yaml +0 -1
- package/packs/z-image-base-inpaint/install-runpod.sh +0 -1
- package/packs/z-image-base-inpaint/install-windows.bat +0 -1
- package/packs/z-image-base-inpaint/manifest.yaml +0 -1
- package/packs/z-image-base-txt2img/install-runpod.sh +0 -1
- package/packs/z-image-base-txt2img/install-windows.bat +0 -1
- package/packs/z-image-base-txt2img/manifest.yaml +0 -1
- package/packs/z-image-turbo/install-runpod.sh +0 -1
- package/packs/z-image-turbo/install-windows.bat +0 -1
- package/packs/z-image-turbo/manifest.yaml +0 -1
- package/packs/z-image-turbo-combo/install-runpod.sh +0 -1
- package/packs/z-image-turbo-combo/install-windows.bat +0 -1
- package/packs/z-image-turbo-combo/manifest.yaml +0 -1
- package/packs/z-image-turbo-controlnet/install-runpod.sh +0 -1
- package/packs/z-image-turbo-controlnet/install-windows.bat +0 -1
- package/packs/z-image-turbo-controlnet/manifest.yaml +0 -1
- package/packs/z-image-turbo-detail-daemon/install-runpod.sh +0 -1
- package/packs/z-image-turbo-detail-daemon/install-windows.bat +0 -1
- package/packs/z-image-turbo-detail-daemon/manifest.yaml +0 -1
- package/packs/z-image-turbo-img2img/install-runpod.sh +0 -1
- package/packs/z-image-turbo-img2img/install-windows.bat +0 -1
- package/packs/z-image-turbo-img2img/manifest.yaml +0 -1
- package/packs/z-image-turbo-inpainting/install-runpod.sh +0 -1
- package/packs/z-image-turbo-inpainting/install-windows.bat +0 -1
- package/packs/z-image-turbo-inpainting/manifest.yaml +0 -1
- package/packs/z-image-turbo-txt2img/install-runpod.sh +0 -1
- package/packs/z-image-turbo-txt2img/install-windows.bat +0 -1
- package/packs/z-image-turbo-txt2img/manifest.yaml +0 -1
- package/packs/z-image-xy-plot/install-runpod.sh +0 -1
- package/packs/z-image-xy-plot/install-windows.bat +0 -1
- package/packs/z-image-xy-plot/manifest.yaml +0 -1
- package/plugin/skills/flux-txt2img/SKILL.md +9 -4
- package/plugin/skills/wan-scail-replacement/SKILL.md +95 -0
- package/scripts/sync-agents.mjs +18 -8
- package/scripts/test-packs.sh +8 -3
|
@@ -17,10 +17,58 @@
|
|
|
17
17
|
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
18
18
|
import { WebSocketServer, WebSocket } from "ws";
|
|
19
19
|
import { logger } from "../utils/logger.js";
|
|
20
|
+
import { compareSemver } from "./self-update.js";
|
|
20
21
|
export const DEFAULT_BRIDGE_PORT = 9101;
|
|
21
22
|
/** Panel build that first implements the full graph_* / ui_* bridge command set.
|
|
22
|
-
*
|
|
23
|
+
* Used as the DEFAULT minimum for any bridge command not listed in
|
|
24
|
+
* BRIDGE_CMD_MIN_PANEL_VERSION below. Individual commands shipped EARLIER than
|
|
25
|
+
* this (see the map) and must quote their own, lower minimum. */
|
|
23
26
|
export const MIN_PANEL_VERSION_FOR_BRIDGE_COMMANDS = "0.11.4";
|
|
27
|
+
/** The panel version each bridge command was ACTUALLY introduced in (from the
|
|
28
|
+
* comfyui-mcp-panel CHANGELOG). The old code quoted a single blanket 0.11.4 for
|
|
29
|
+
* EVERY command, which is wrong for commands that shipped much earlier —
|
|
30
|
+
* graph_outline has existed since panel 0.4.6, so a "too old — update to ≥0.11.4"
|
|
31
|
+
* verdict for it is a false, inflated requirement (#352). Anything not listed
|
|
32
|
+
* falls back to MIN_PANEL_VERSION_FOR_BRIDGE_COMMANDS (the full-set baseline). */
|
|
33
|
+
export const BRIDGE_CMD_MIN_PANEL_VERSION = {
|
|
34
|
+
graph_outline: "0.4.6",
|
|
35
|
+
graph_find_nodes: "0.4.6",
|
|
36
|
+
graph_query: "0.7.0",
|
|
37
|
+
graph_serialize: "0.8.2",
|
|
38
|
+
};
|
|
39
|
+
/** The minimum panel version that supports `cmd`. */
|
|
40
|
+
export function minPanelVersionForCmd(cmd) {
|
|
41
|
+
return BRIDGE_CMD_MIN_PANEL_VERSION[cmd] ?? MIN_PANEL_VERSION_FOR_BRIDGE_COMMANDS;
|
|
42
|
+
}
|
|
43
|
+
/** A version string compareSemver can actually parse. compareSemver returns 0
|
|
44
|
+
* BOTH for "equal" and for "unparseable", so callers that must distinguish an
|
|
45
|
+
* unparseable input from a genuine tie (like panelSupportsCmd's `>= 0`) have to
|
|
46
|
+
* screen the input first — otherwise `panel_version: "dev"` would compare as 0
|
|
47
|
+
* and be mistaken for a match. */
|
|
48
|
+
const SEMVER_RE = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/;
|
|
49
|
+
/** True when the panel ADVERTISED a PARSEABLE version (in its `hello`) that already
|
|
50
|
+
* meets `cmd`'s real minimum — i.e. this panel is new enough to support the command,
|
|
51
|
+
* so an "Unknown command" reply is NOT an age problem and must never be rewritten
|
|
52
|
+
* into a "too old — update your panel" verdict (#352 false negative). Missing OR
|
|
53
|
+
* unparseable panelVersion → we can't PROVE it's new enough, so treat as not-proven
|
|
54
|
+
* (fall through to the honest too-old path, which quotes the correct minimum, and
|
|
55
|
+
* keep the #236 learning path intact). */
|
|
56
|
+
function panelSupportsCmd(cmd, panelVersion) {
|
|
57
|
+
if (!panelVersion || !SEMVER_RE.test(panelVersion.trim()))
|
|
58
|
+
return false;
|
|
59
|
+
return compareSemver(panelVersion, minPanelVersionForCmd(cmd)) >= 0;
|
|
60
|
+
}
|
|
61
|
+
/** The actionable "update your panel" message for a command a connected panel has
|
|
62
|
+
* been discovered NOT to support. Shared by the reactive path (the panel's own
|
|
63
|
+
* "Unknown command" reply, mapped by makeUnknownCommandError) and the proactive
|
|
64
|
+
* gate (#236 — a command already known-unsupported for THIS connection, from an
|
|
65
|
+
* earlier call in the same session) so both produce the identical message. The
|
|
66
|
+
* quoted minimum is COMMAND-SPECIFIC (#352) — not a blanket 0.11.4. */
|
|
67
|
+
function buildPanelTooOldError(cmd, panelVersion) {
|
|
68
|
+
const detected = panelVersion ? ` (detected ${panelVersion})` : "";
|
|
69
|
+
return new Error(`This ComfyUI-MCP panel is too old for "${cmd}"${detected} — update the ComfyUI-MCP panel ` +
|
|
70
|
+
`to ≥${minPanelVersionForCmd(cmd)} (ComfyUI Manager → update comfyui-mcp panel), then reconnect.`);
|
|
71
|
+
}
|
|
24
72
|
/**
|
|
25
73
|
* A panel replies `{ ok: false, error: 'Unknown command "<cmd>"' }` when the
|
|
26
74
|
* orchestrator dispatches a bridge command the (older) panel doesn't register —
|
|
@@ -40,9 +88,99 @@ export function makeUnknownCommandError(error, panelVersion) {
|
|
|
40
88
|
if (!m)
|
|
41
89
|
return null;
|
|
42
90
|
const cmd = m[1];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
91
|
+
// #352 FALSE-NEGATIVE GUARD: if the panel advertised a version that already
|
|
92
|
+
// meets this command's real minimum, the panel is NOT too old — an "Unknown
|
|
93
|
+
// command" reply here means something else (a genuinely retired/renamed command,
|
|
94
|
+
// or a transient), so do NOT rewrite it into a bogus "update your panel" verdict
|
|
95
|
+
// (which would also POISON the #236 unsupported-cmd gate against a capable panel).
|
|
96
|
+
// Return null so the raw error surfaces and the gate is never poisoned.
|
|
97
|
+
if (panelSupportsCmd(cmd, panelVersion))
|
|
98
|
+
return null;
|
|
99
|
+
return buildPanelTooOldError(cmd, panelVersion);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Bridge commands with NO side effect — safe to re-dispatch after a reconnect and
|
|
103
|
+
* safe to WAIT ON longer (a read can't be double-applied). Module-level so both
|
|
104
|
+
* the mutating-vs-read classification and the default-timeout policy share one
|
|
105
|
+
* authoritative list. Everything not listed is treated as mutating.
|
|
106
|
+
*/
|
|
107
|
+
export const BRIDGE_READONLY_CMDS = new Set([
|
|
108
|
+
"graph_serialize",
|
|
109
|
+
"graph_outline",
|
|
110
|
+
"graph_get_errors",
|
|
111
|
+
"graph_get_state",
|
|
112
|
+
"graph_get_subgraph",
|
|
113
|
+
"graph_view_selected",
|
|
114
|
+
"graph_view_nodes_in_viewport",
|
|
115
|
+
"graph_prompt_director_audit",
|
|
116
|
+
"graph_query",
|
|
117
|
+
"civitai_results",
|
|
118
|
+
"get_todo",
|
|
119
|
+
]);
|
|
120
|
+
/** Tight default reply timeout for a MUTATING command with no explicit timeout —
|
|
121
|
+
* fail fast so the agent isn't blocked on a stuck write. */
|
|
122
|
+
export const BRIDGE_DEFAULT_TIMEOUT_MS = 6000;
|
|
123
|
+
/** More tolerant default for a READ (idempotent) command with no explicit timeout.
|
|
124
|
+
* A legitimately busy-but-alive panel main thread — e.g. Preview3D parsing a large
|
|
125
|
+
* FBX — can take many seconds to service a graph_query; failing it at the tight
|
|
126
|
+
* 6s default surfaced a FALSE "tab backgrounded or frozen" timeout even though a
|
|
127
|
+
* retry moments later succeeded (#357). Reads have no side effect, so waiting
|
|
128
|
+
* longer is safe; a genuinely dead/frozen tab still times out, just at this bound.
|
|
129
|
+
* Note: a WebSocket pong is answered by the browser network stack even while JS is
|
|
130
|
+
* blocked, so it can't distinguish busy-JS from dead-JS — a tolerant read timeout,
|
|
131
|
+
* not pong-liveness, is the correct lever here. */
|
|
132
|
+
export const BRIDGE_READ_DEFAULT_TIMEOUT_MS = 20_000;
|
|
133
|
+
/** The default reply timeout to use for `cmd` when the caller passed none. Read
|
|
134
|
+
* ops get the tolerant bound; everything else stays tight (#357). */
|
|
135
|
+
export function defaultBridgeTimeoutMs(cmdName) {
|
|
136
|
+
return BRIDGE_READONLY_CMDS.has(cmdName)
|
|
137
|
+
? BRIDGE_READ_DEFAULT_TIMEOUT_MS
|
|
138
|
+
: BRIDGE_DEFAULT_TIMEOUT_MS;
|
|
139
|
+
}
|
|
140
|
+
/** Normalize a WebSocket handshake `Origin` header into a canonical scheme://host:port
|
|
141
|
+
* string, or undefined when it is absent, the opaque literal `"null"` (a sandboxed /
|
|
142
|
+
* file:// / data: origin, which proves nothing), or unparseable. The browser sets this
|
|
143
|
+
* header on the upgrade and forbids page JS from overriding it, so a value here is
|
|
144
|
+
* server-trusted provenance of the page the socket runs in. Host case is lowercased and
|
|
145
|
+
* the default port made explicit so it compares cleanly against a probe base. */
|
|
146
|
+
function normalizeHandshakeOrigin(raw) {
|
|
147
|
+
const val = Array.isArray(raw) ? raw[0] : raw;
|
|
148
|
+
if (typeof val !== "string" || val === "" || val.toLowerCase() === "null")
|
|
149
|
+
return undefined;
|
|
150
|
+
try {
|
|
151
|
+
const u = new URL(val);
|
|
152
|
+
const port = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
153
|
+
return `${u.protocol}//${u.hostname.toLowerCase()}:${port}`;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** AUTHORITATIVE, TYPED dispatch outcome carried on a bridge command REJECTION so a
|
|
160
|
+
* caller never has to infer it from message text:
|
|
161
|
+
* - `false` = the command was NEVER written to the socket (a PRE-write send() failure);
|
|
162
|
+
* - `true` = the command WAS written and the socket then died before a reply (a
|
|
163
|
+
* POST-write mid-command OUTCOME-UNKNOWN drop).
|
|
164
|
+
* Absent on all other errors (a reply-timeout, a genuine refusal, "panel gone", etc.). */
|
|
165
|
+
const DISPATCHED = Symbol.for("comfyui-mcp.bridge.dispatched");
|
|
166
|
+
/** Tag an error with the typed {@link DISPATCHED} outcome and return it (for throw/reject). */
|
|
167
|
+
export function markDispatched(err, dispatched) {
|
|
168
|
+
Object.defineProperty(err, DISPATCHED, {
|
|
169
|
+
value: dispatched,
|
|
170
|
+
enumerable: false,
|
|
171
|
+
configurable: true,
|
|
172
|
+
});
|
|
173
|
+
return err;
|
|
174
|
+
}
|
|
175
|
+
/** Read the typed dispatch outcome from an error, or undefined if it carries none.
|
|
176
|
+
* A reboot readiness check uses this to classify a PRE-write send failure
|
|
177
|
+
* (`false`) categorically as NOT-dispatched — never as an accepted/dropped reboot —
|
|
178
|
+
* regardless of any post-write phrase its message text may quote. */
|
|
179
|
+
export function dispatchOutcomeOf(err) {
|
|
180
|
+
if (err == null || (typeof err !== "object" && typeof err !== "function"))
|
|
181
|
+
return undefined;
|
|
182
|
+
const v = err[DISPATCHED];
|
|
183
|
+
return typeof v === "boolean" ? v : undefined;
|
|
46
184
|
}
|
|
47
185
|
/**
|
|
48
186
|
* Frame `type`s that are safe to MIRROR to a tab's viewers — genuine shared-session
|
|
@@ -133,6 +271,20 @@ export class UiBridge {
|
|
|
133
271
|
missedFrames = new Map();
|
|
134
272
|
static MAX_MISSED_FRAMES = 100;
|
|
135
273
|
pending = new Map();
|
|
274
|
+
/** ask_user card sends (rid → {ask_id, ts}): lets a reply that validates AFTER
|
|
275
|
+
* the reply timer fired (dropped from `pending`) still be buffered for the
|
|
276
|
+
* caller, instead of being discarded as a "late reply for a timed-out command"
|
|
277
|
+
* (#486). Timestamped so an abandoned mapping (timeout/disconnect/send-failure
|
|
278
|
+
* whose late reply never arrives) is TTL-pruned rather than kept forever. */
|
|
279
|
+
askRidToId = new Map();
|
|
280
|
+
/** Buffered late-but-valid ask_user answers (ask_id → result), drained by the
|
|
281
|
+
* caller via takeLateAskReply(). Bounded by a short TTL — a stale unclaimed
|
|
282
|
+
* answer is pruned rather than kept forever. */
|
|
283
|
+
lateAskReplies = new Map();
|
|
284
|
+
/** TTL for a buffered late ask answer / an unresolved rid→ask_id mapping. Long
|
|
285
|
+
* enough to cover a slow human pick within the MCP tools/call budget, short
|
|
286
|
+
* enough that abandoned entries don't accumulate. */
|
|
287
|
+
static LATE_ASK_TTL_MS = 5 * 60 * 1000;
|
|
136
288
|
/** In-flight IDEMPOTENT reads whose socket dropped mid-command, parked per tabId
|
|
137
289
|
* waiting a bounded grace for that tab to reconnect so we can re-dispatch them
|
|
138
290
|
* (resume) instead of hard-failing. Never holds mutating commands. */
|
|
@@ -142,17 +294,10 @@ export class UiBridge {
|
|
|
142
294
|
static RECONNECT_GRACE_MS = 4000;
|
|
143
295
|
/** Bridge commands with no side effect — safe to re-dispatch after a reconnect.
|
|
144
296
|
* Everything else is treated as mutating (no auto-retry) so a render/edit is
|
|
145
|
-
* never silently double-applied.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"graph_get_errors",
|
|
150
|
-
"graph_get_subgraph",
|
|
151
|
-
"graph_prompt_director_audit",
|
|
152
|
-
"graph_query",
|
|
153
|
-
"civitai_results",
|
|
154
|
-
"get_todo",
|
|
155
|
-
]);
|
|
297
|
+
* never silently double-applied. Single source of truth at module scope
|
|
298
|
+
* (BRIDGE_READONLY_CMDS) so the mutating flag and the default-timeout policy
|
|
299
|
+
* can never drift apart. */
|
|
300
|
+
static READONLY_CMDS = BRIDGE_READONLY_CMDS;
|
|
156
301
|
portInUse = false;
|
|
157
302
|
bindRetryTimer = null;
|
|
158
303
|
heartbeatTimer = null;
|
|
@@ -332,14 +477,20 @@ export class UiBridge {
|
|
|
332
477
|
this.readyResolve = null;
|
|
333
478
|
}
|
|
334
479
|
});
|
|
335
|
-
wss.on("connection", (sock) => {
|
|
480
|
+
wss.on("connection", (sock, req) => {
|
|
336
481
|
// Keepalive bookkeeping for the SERVER-LEVEL heartbeat loop, which only
|
|
337
482
|
// iterates real wss.clients (relay-mediated shim connections aren't real
|
|
338
483
|
// sockets bound by this server, so they're intentionally excluded — see
|
|
339
484
|
// relay-client.ts / the relay README for why that's believed low-risk).
|
|
340
485
|
this.missedPongs.set(sock, 0);
|
|
341
486
|
sock.on("pong", () => this.missedPongs.set(sock, 0));
|
|
342
|
-
|
|
487
|
+
// SERVER-OBSERVED handshake Origin (the browser sets it and forbids page JS from
|
|
488
|
+
// overriding it) — the trusted proof of which page this socket runs in, distinct
|
|
489
|
+
// from the spoofable hello.comfyui_url (#509 self-probe gate).
|
|
490
|
+
const handshakeOrigin = normalizeHandshakeOrigin(req?.headers?.origin);
|
|
491
|
+
// Trusted-local ONLY when the primary listener is a token-less loopback bind
|
|
492
|
+
// (no LAN bind, no tunnel/secure token in front) — see handleConnection's doc.
|
|
493
|
+
this.handleConnection(sock, !this.token && isLoopbackBindHost(this.host), handshakeOrigin);
|
|
343
494
|
});
|
|
344
495
|
}
|
|
345
496
|
/**
|
|
@@ -411,7 +562,18 @@ export class UiBridge {
|
|
|
411
562
|
});
|
|
412
563
|
});
|
|
413
564
|
}
|
|
414
|
-
|
|
565
|
+
/**
|
|
566
|
+
* @param local SERVER-TRUSTED provenance: true only when the socket arrived on
|
|
567
|
+
* the token-less loopback primary listener, i.e. a browser genuinely on the
|
|
568
|
+
* orchestrator's own host. A token-gated/tunnel-fronted primary, a relay shim,
|
|
569
|
+
* and any LAN/pairing listener are all `false` — a browser reaching those can
|
|
570
|
+
* sit on a DIFFERENT machine yet advertise its OWN 127.0.0.1 ComfyUI, which
|
|
571
|
+
* must never be treated as the orchestrator's local host (#509).
|
|
572
|
+
* @param serverOrigin SERVER-OBSERVED handshake `Origin` (scheme://host:port), captured
|
|
573
|
+
* from the WebSocket upgrade — NOT client-supplied. Undefined for non-browser/relay
|
|
574
|
+
* connections. Bound to the tab so the reboot self-probe can trust which page it fronts.
|
|
575
|
+
*/
|
|
576
|
+
handleConnection(sock, local = false, serverOrigin) {
|
|
415
577
|
// The connection is anonymous until its hello frame names a tab id.
|
|
416
578
|
let tabId = null;
|
|
417
579
|
// A socket's KIND (canvas-owning panel vs headless viewer) is pinned on its
|
|
@@ -518,6 +680,25 @@ export class UiBridge {
|
|
|
518
680
|
panelVersion: typeof msg.panel_version === "string"
|
|
519
681
|
? (msg.panel_version || undefined)
|
|
520
682
|
: existing?.panelVersion,
|
|
683
|
+
// Fresh per hello — see the field's doc comment (#236).
|
|
684
|
+
unsupportedCmds: new Set(),
|
|
685
|
+
// window.location the browser was served from — the ComfyUI instance THIS
|
|
686
|
+
// tab fronts. Preserved across a SAME-SOCKET re-hello that omits it, but
|
|
687
|
+
// NEVER inherited by a DIFFERENT socket reusing this (possibly recurring
|
|
688
|
+
// wf:<hash>) tab id — that could let an unrelated instance's origin certify
|
|
689
|
+
// this tab's restart (#509, codex: cross-ownership inheritance).
|
|
690
|
+
originUrl: typeof msg.comfyui_url === "string" &&
|
|
691
|
+
msg.comfyui_url
|
|
692
|
+
? msg.comfyui_url
|
|
693
|
+
: existing?.sock === sock
|
|
694
|
+
? existing?.originUrl
|
|
695
|
+
: undefined,
|
|
696
|
+
// SERVER-OBSERVED handshake Origin of THIS socket (from the WS upgrade, not the
|
|
697
|
+
// hello) — bound per-socket. A same-socket re-hello keeps it; a DIFFERENT socket
|
|
698
|
+
// taking over the id carries its OWN handshake Origin (never inherits the old).
|
|
699
|
+
serverOrigin: existing?.sock === sock ? (serverOrigin ?? existing?.serverOrigin) : serverOrigin,
|
|
700
|
+
// Server-trusted provenance of THIS socket (not client-controlled).
|
|
701
|
+
local,
|
|
521
702
|
});
|
|
522
703
|
if (incomingHeadless)
|
|
523
704
|
this.headlessSeen.add(tabId);
|
|
@@ -560,10 +741,24 @@ export class UiBridge {
|
|
|
560
741
|
const rid = typeof msg.rid === "string" ? msg.rid : undefined;
|
|
561
742
|
if (rid) {
|
|
562
743
|
const p = this.pending.get(rid);
|
|
563
|
-
if (!p)
|
|
564
|
-
|
|
744
|
+
if (!p) {
|
|
745
|
+
// Late reply for a timed-out command. If it was an ask_user card whose
|
|
746
|
+
// caller may still be within the MCP tools/call budget, BUFFER the
|
|
747
|
+
// validated answer keyed by its ask_id so the caller can retrieve it via
|
|
748
|
+
// takeLateAskReply() instead of losing it (#486). Everything else drops.
|
|
749
|
+
const entry = this.askRidToId.get(rid);
|
|
750
|
+
if (entry) {
|
|
751
|
+
this.askRidToId.delete(rid);
|
|
752
|
+
if (msg.ok) {
|
|
753
|
+
this.pruneLateAsk();
|
|
754
|
+
this.lateAskReplies.set(entry.askId, { result: msg.result, ts: Date.now() });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
565
759
|
clearTimeout(p.timer);
|
|
566
760
|
this.pending.delete(rid);
|
|
761
|
+
this.askRidToId.delete(rid);
|
|
567
762
|
if (msg.ok) {
|
|
568
763
|
p.resolve(msg.result);
|
|
569
764
|
}
|
|
@@ -726,6 +921,48 @@ export class UiBridge {
|
|
|
726
921
|
resolveActiveTabId() {
|
|
727
922
|
return this.resolveTarget().tabId;
|
|
728
923
|
}
|
|
924
|
+
/** The ComfyUI origin the given tab's browser was served from (`comfyui_url` in
|
|
925
|
+
* its `hello`), or undefined if the tab never advertised one / is unknown. Lets a
|
|
926
|
+
* tool HTTP-probe the EXACT instance THIS tab fronts rather than the process-
|
|
927
|
+
* global target a different instance may have retargeted (#509). Resolves the id
|
|
928
|
+
* through the SAME prefix + migration-alias path as command routing (resolveTarget)
|
|
929
|
+
* so a migrated session still finds its origin; never throws. */
|
|
930
|
+
tabOrigin(tabId) {
|
|
931
|
+
try {
|
|
932
|
+
return this.resolveTarget(tabId).originUrl;
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
return undefined;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/** SERVER-OBSERVED origin (scheme://host:port) of the given tab's WebSocket handshake —
|
|
939
|
+
* the page the browser was actually serving when it opened this socket. Unlike
|
|
940
|
+
* {@link tabOrigin} (client-supplied `hello.comfyui_url`, spoofable), the browser sets
|
|
941
|
+
* the handshake `Origin` and blocks page JS from forging it, so this is the TRUSTED
|
|
942
|
+
* proof of which ComfyUI a real tab fronts — the origin the reboot self-probe binds to
|
|
943
|
+
* (#509). Undefined when the handshake carried no usable Origin. Resolves migration
|
|
944
|
+
* aliases like tabOrigin; never throws. */
|
|
945
|
+
tabServerOrigin(tabId) {
|
|
946
|
+
try {
|
|
947
|
+
return this.resolveTarget(tabId).serverOrigin;
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/** SERVER-TRUSTED: true only when this tab's socket arrived on the token-less
|
|
954
|
+
* loopback primary listener (a browser on the orchestrator's OWN host), so its
|
|
955
|
+
* advertised loopback ComfyUI origin really is the orchestrator's local host and
|
|
956
|
+
* may be directly health-probed (#509). Relay/tunnel/LAN/pairing tabs → false.
|
|
957
|
+
* Resolves migration aliases like tabOrigin; unknown → false. */
|
|
958
|
+
tabIsLocal(tabId) {
|
|
959
|
+
try {
|
|
960
|
+
return this.resolveTarget(tabId).local === true;
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
729
966
|
/** True when the tab advertised itself as a canvas-less (mobile/remote) client
|
|
730
967
|
* in its `hello`. Unknown tabs → false. */
|
|
731
968
|
isHeadless(tabId) {
|
|
@@ -734,6 +971,45 @@ export class UiBridge {
|
|
|
734
971
|
// bytes-less viewRef) and is renderable when the mailbox flushes to the phone.
|
|
735
972
|
return this.conns.get(tabId)?.headless === true || this.headlessSeen.has(tabId);
|
|
736
973
|
}
|
|
974
|
+
/** Drop expired late-ask entries (buffered answers + unresolved rid mappings) so
|
|
975
|
+
* an abandoned card never leaks memory. Cheap; called on each ask send/take. */
|
|
976
|
+
pruneLateAsk() {
|
|
977
|
+
const now = Date.now();
|
|
978
|
+
for (const [id, e] of this.lateAskReplies) {
|
|
979
|
+
if (now - e.ts > UiBridge.LATE_ASK_TTL_MS)
|
|
980
|
+
this.lateAskReplies.delete(id);
|
|
981
|
+
}
|
|
982
|
+
// TTL-prune abandoned rid→ask_id mappings (a card that timed out / dropped and
|
|
983
|
+
// whose late reply never came), so they don't linger past the window a caller
|
|
984
|
+
// could still claim them.
|
|
985
|
+
for (const [rid, e] of this.askRidToId) {
|
|
986
|
+
if (now - e.ts > UiBridge.LATE_ASK_TTL_MS)
|
|
987
|
+
this.askRidToId.delete(rid);
|
|
988
|
+
}
|
|
989
|
+
// Belt-and-suspenders cardinality cap in case of a burst of asks within one TTL
|
|
990
|
+
// window — drop the oldest-inserted mappings so the map can never grow unbounded.
|
|
991
|
+
if (this.askRidToId.size > 256) {
|
|
992
|
+
const excess = this.askRidToId.size - 256;
|
|
993
|
+
let i = 0;
|
|
994
|
+
for (const rid of this.askRidToId.keys()) {
|
|
995
|
+
if (i++ >= excess)
|
|
996
|
+
break;
|
|
997
|
+
this.askRidToId.delete(rid);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
/** Retrieve (and remove) a late-but-valid ask_user answer buffered for `askId`
|
|
1002
|
+
* after the card-reply timeout, or undefined if none arrived. The panel_ask
|
|
1003
|
+
* handler polls this for a bounded grace so a slow-but-valid pick is honored
|
|
1004
|
+
* rather than discarded (#486). */
|
|
1005
|
+
takeLateAskReply(askId) {
|
|
1006
|
+
this.pruneLateAsk();
|
|
1007
|
+
const e = this.lateAskReplies.get(askId);
|
|
1008
|
+
if (!e)
|
|
1009
|
+
return undefined;
|
|
1010
|
+
this.lateAskReplies.delete(askId);
|
|
1011
|
+
return e.result;
|
|
1012
|
+
}
|
|
737
1013
|
/** All currently connected tabs, most recent hello last. */
|
|
738
1014
|
tabs() {
|
|
739
1015
|
return Array.from(this.conns.values()).map((c) => ({
|
|
@@ -837,7 +1113,10 @@ export class UiBridge {
|
|
|
837
1113
|
}
|
|
838
1114
|
}
|
|
839
1115
|
send(cmd, opts = {}) {
|
|
840
|
-
|
|
1116
|
+
// #357: read (idempotent) ops get a more tolerant default so a busy-but-alive
|
|
1117
|
+
// panel main thread (e.g. Preview3D loading a large FBX) isn't declared frozen;
|
|
1118
|
+
// mutating ops keep the tight default. An explicit opts.timeoutMs always wins.
|
|
1119
|
+
const timeoutMs = opts.timeoutMs ?? defaultBridgeTimeoutMs(cmd.cmd);
|
|
841
1120
|
let conn;
|
|
842
1121
|
try {
|
|
843
1122
|
conn = this.resolveTarget(opts.tabId);
|
|
@@ -851,6 +1130,15 @@ export class UiBridge {
|
|
|
851
1130
|
}
|
|
852
1131
|
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
853
1132
|
}
|
|
1133
|
+
// #236 — proactively gate a command this exact connection has already proven
|
|
1134
|
+
// unsupported (see Conn.unsupportedCmds), instead of dispatching it again just
|
|
1135
|
+
// to have the panel reject it and re-parse the same "Unknown command" string.
|
|
1136
|
+
// Never a false gate: membership here is only ever set from a REAL rejection
|
|
1137
|
+
// on THIS connection (in dispatch()'s rejectMapped below), never inferred from
|
|
1138
|
+
// panelVersion alone.
|
|
1139
|
+
if (conn.unsupportedCmds.has(cmd.cmd)) {
|
|
1140
|
+
return Promise.reject(buildPanelTooOldError(cmd.cmd, conn.panelVersion));
|
|
1141
|
+
}
|
|
854
1142
|
if (conn.sock.readyState !== WebSocket.OPEN) {
|
|
855
1143
|
if (opts.tabId && UiBridge.isMailboxable(cmd)) {
|
|
856
1144
|
this.storeMailbox(opts.tabId, cmd);
|
|
@@ -889,17 +1177,38 @@ export class UiBridge {
|
|
|
889
1177
|
}
|
|
890
1178
|
const replyTimeoutMs = Math.min(ctx.timeoutMs, remaining);
|
|
891
1179
|
const rid = randomUUID();
|
|
1180
|
+
// Track an ask_user card's stable ask_id against this attempt's rid so a reply
|
|
1181
|
+
// that validates AFTER the reply timer fires (and drops out of `pending`) can
|
|
1182
|
+
// still be buffered for the caller by rid, not discarded (#486 late answer).
|
|
1183
|
+
const askId = cmd.ask_id;
|
|
1184
|
+
if (typeof askId === "string" && askId) {
|
|
1185
|
+
this.pruneLateAsk();
|
|
1186
|
+
this.askRidToId.set(rid, { askId, ts: Date.now() });
|
|
1187
|
+
}
|
|
892
1188
|
// Rewrite an old-panel "Unknown command" rejection into an actionable
|
|
893
1189
|
// update-your-panel message (see makeUnknownCommandError). Applied to the
|
|
894
1190
|
// reply-error path only; the happy path and genuine command errors are
|
|
895
1191
|
// passed through untouched.
|
|
896
1192
|
const rejectMapped = (err) => {
|
|
897
1193
|
const friendly = makeUnknownCommandError(err.message, conn.panelVersion);
|
|
1194
|
+
if (friendly) {
|
|
1195
|
+
// Learned, not assumed (#236) — this exact connection just proved it
|
|
1196
|
+
// doesn't support cmd.cmd, so every later call in this session is gated
|
|
1197
|
+
// proactively (see the `send()` preflight above) instead of round-
|
|
1198
|
+
// tripping to the panel again.
|
|
1199
|
+
conn.unsupportedCmds.add(cmd.cmd);
|
|
1200
|
+
}
|
|
898
1201
|
ctx.reject(friendly ?? err);
|
|
899
1202
|
};
|
|
900
1203
|
const timer = setTimeout(() => {
|
|
901
1204
|
this.pending.delete(rid);
|
|
902
|
-
|
|
1205
|
+
// This timer only fires AFTER sock.send() below returned successfully — the command
|
|
1206
|
+
// WAS WRITTEN to the socket; the tab (possibly backgrounded/frozen) merely didn't
|
|
1207
|
+
// reply in time and may still apply it. So this is a POST-write outcome: tag it
|
|
1208
|
+
// dispatched:true so a mutating caller (e.g. comfy_reboot readiness) treats it as an
|
|
1209
|
+
// accepted-but-unacked dispatch and verifies by observation, rather than mistaking a
|
|
1210
|
+
// genuinely-sent command for one that never went out (#509).
|
|
1211
|
+
ctx.reject(markDispatched(new Error(`Panel tab ${conn.tabId.slice(0, 8)} did not reply to "${cmd.cmd}" within ${ctx.timeoutMs} ms — the ComfyUI tab may be backgrounded or frozen`), true));
|
|
903
1212
|
}, replyTimeoutMs);
|
|
904
1213
|
const pending = {
|
|
905
1214
|
resolve: ctx.resolve,
|
|
@@ -916,7 +1225,15 @@ export class UiBridge {
|
|
|
916
1225
|
catch (err) {
|
|
917
1226
|
clearTimeout(timer);
|
|
918
1227
|
this.pending.delete(rid);
|
|
919
|
-
|
|
1228
|
+
// The write to the socket FAILED — the command was NEVER transmitted, so nothing
|
|
1229
|
+
// was dispatched (distinct from a POST-write mid-command drop). Surface that
|
|
1230
|
+
// explicitly so callers don't mistake it for an in-flight/accepted command (a
|
|
1231
|
+
// reboot readiness check must NOT treat a pre-write send failure as a fired reboot).
|
|
1232
|
+
// Carry an AUTHORITATIVE TYPED flag `dispatched:false` so a caller classifies this
|
|
1233
|
+
// categorically — never by string-matching a detail that might quote a post-write
|
|
1234
|
+
// phrase ("OUTCOME UNKNOWN") from the underlying socket error.
|
|
1235
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1236
|
+
ctx.reject(markDispatched(new Error(`failed to send "${cmd.cmd}" to panel tab ${conn.tabId.slice(0, 8)} — the command was NOT dispatched (${detail})`), false));
|
|
920
1237
|
}
|
|
921
1238
|
}
|
|
922
1239
|
/** A pending command's socket died before its reply arrived. Decide, WITHOUT
|
|
@@ -958,7 +1275,7 @@ export class UiBridge {
|
|
|
958
1275
|
// bare failure invites a blind retry that double-applies the action (e.g. a
|
|
959
1276
|
// second render). Say the outcome is UNKNOWN so the caller verifies first.
|
|
960
1277
|
if (ctx.mutating) {
|
|
961
|
-
ctx.reject(new Error(`panel tab ${short} disconnected mid-command ("${cmd}") — OUTCOME UNKNOWN: the command was already sent, so the panel may have applied it (for a run, ComfyUI may already be rendering). Verify before retrying (e.g. check get_queue / list_output_images) instead of re-issuing it blindly.`));
|
|
1278
|
+
ctx.reject(markDispatched(new Error(`panel tab ${short} disconnected mid-command ("${cmd}") — OUTCOME UNKNOWN: the command was already sent, so the panel may have applied it (for a run, ComfyUI may already be rendering). Verify before retrying (e.g. check get_queue / list_output_images) instead of re-issuing it blindly.`), true));
|
|
962
1279
|
}
|
|
963
1280
|
else {
|
|
964
1281
|
ctx.reject(new Error(`panel tab ${short} disconnected mid-command ("${cmd}") — panel genuinely gone; retry once a tab is connected`));
|