comfyui-mcp 0.48.19 → 0.48.20
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/dist/orchestrator/panel-tools.js +270 -11
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/comfy-cli.js +26 -2
- package/dist/services/comfy-cli.js.map +1 -1
- package/dist/services/node-dev.js +15 -6
- package/dist/services/node-dev.js.map +1 -1
- package/dist/services/node-management.js +60 -0
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/process-control.js +46 -2
- package/dist/services/process-control.js.map +1 -1
- package/dist/tools/comfy-cli.js +20 -8
- package/dist/tools/comfy-cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -37,6 +37,8 @@ import { getNsfwConsent, setNsfwConsent } from "../services/panel-settings.js";
|
|
|
37
37
|
import { QueueMonitor } from "../services/queue-monitor.js";
|
|
38
38
|
import { getObjectInfo, backfillObjectInfo, resetClient, resetObjectInfoCache, } from "../comfyui/client.js";
|
|
39
39
|
import { convertUiToApi, collectNodeTypes } from "../services/workflow-converter.js";
|
|
40
|
+
import { restartComfyUI } from "../services/process-control.js";
|
|
41
|
+
import { isRemoteMode } from "../config.js";
|
|
40
42
|
import { sliceWorkflow } from "../services/workflow-slicer.js";
|
|
41
43
|
import { validateA2UISpecServer } from "../services/a2ui-spec.js";
|
|
42
44
|
/** Treat these as an affirmative answer to the adult-content consent card. */
|
|
@@ -103,6 +105,30 @@ export function rebootDropped(res) {
|
|
|
103
105
|
// unrelated tab reconnection makes readiness pass. Those return verbatim.
|
|
104
106
|
return /disconnected mid-command|OUTCOME UNKNOWN|ECONNRESET|socket hang up|premature close|other side closed|ECONNABORTED|EPIPE/i.test(text);
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* True when a comfy_reboot ToolResult is a NON-error, NON-fired refusal whose
|
|
110
|
+
* cause is that the panel could reach NO ComfyUI-Manager reboot endpoint — i.e.
|
|
111
|
+
* every Manager reboot route answered 404/405 (the classic legacy Manager 3.x
|
|
112
|
+
* symptom: `POST /v2/manager/reboot → 405; GET /manager/reboot → 404`,
|
|
113
|
+
* panel #253/#266 and this repo #425). This is distinct from:
|
|
114
|
+
* - a busy-guard refusal (a generation is running) — its text speaks to the
|
|
115
|
+
* queue/generation, never to a "reboot endpoint", so it does NOT match; and
|
|
116
|
+
* - a Manager-security 403 refusal — that speaks to "security"/"forbidden".
|
|
117
|
+
* Only a no-endpoint refusal is safe to retry through the headless managed
|
|
118
|
+
* restart (kill + relaunch), and only for a LOCAL, process-controllable target.
|
|
119
|
+
*/
|
|
120
|
+
export function rebootNoEndpoint(res) {
|
|
121
|
+
if (res?.isError)
|
|
122
|
+
return false;
|
|
123
|
+
const text = res?.content?.find((c) => c.type === "text")?.text ?? "";
|
|
124
|
+
// A busy-guard / security refusal must never be treated as "no endpoint" — a
|
|
125
|
+
// kill+relaunch fallback would abort a running render or defeat the security
|
|
126
|
+
// gate. Require the reboot-endpoint signature AND the absence of those.
|
|
127
|
+
if (/busy|in progress|generation|queue is|running|security|forbidden|403/i.test(text)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return /reboot endpoint|reboot route|was NOT restarted|no reachable .*reboot/i.test(text);
|
|
131
|
+
}
|
|
106
132
|
let panelRebootTimingOverride = null;
|
|
107
133
|
function parsePositiveNumberEnv(name, fallback) {
|
|
108
134
|
const raw = process.env[name];
|
|
@@ -130,10 +156,71 @@ export const __panelToolsTestHooks = {
|
|
|
130
156
|
setPanelRebootTiming(timing) {
|
|
131
157
|
panelRebootTimingOverride = timing;
|
|
132
158
|
},
|
|
159
|
+
/** Zero out the post-drop retry settle so retry-once tests don't sleep. */
|
|
160
|
+
setRetrySettleMs(ms) {
|
|
161
|
+
retrySettleMsOverride = ms;
|
|
162
|
+
},
|
|
163
|
+
isRetrySafeCmd,
|
|
164
|
+
isTransientReconnectError,
|
|
133
165
|
};
|
|
134
166
|
function sleep(ms) {
|
|
135
167
|
return new Promise((r) => setTimeout(r, ms));
|
|
136
168
|
}
|
|
169
|
+
// ── Post-reconnect retry-once for idempotent panel commands ──────────────────
|
|
170
|
+
// A tool-triggered ComfyUI reboot (#278/#481), a panel_free_vram (#310), or a
|
|
171
|
+
// Manager-backed call racing a post-restart reconnect (#332) can drop the panel
|
|
172
|
+
// tab's transport the instant AFTER a command was dispatched — or replace the
|
|
173
|
+
// tab under a BRAND-NEW socket/tab id with no migration alias, orphaning this
|
|
174
|
+
// session. The bridge's own mid-command resume only helps when the SAME tab id
|
|
175
|
+
// re-hellos; when the id changed, the in-flight command surfaces a bare
|
|
176
|
+
// "no connected tab" / "disconnected mid-command … genuinely gone" / "Failed to
|
|
177
|
+
// fetch" and the agent is told to hand-call panel_set_workflow_target(current).
|
|
178
|
+
//
|
|
179
|
+
// For commands that are SAFE to re-issue (idempotent reads, plus idempotent
|
|
180
|
+
// UI-state writes like set_todo that fully REPLACE state), we transparently
|
|
181
|
+
// rebind onto the now-live tab (ensureReachable) and retry ONCE after a short
|
|
182
|
+
// settle. Mutating graph edits (add_node/connect/set_widget/…) are deliberately
|
|
183
|
+
// EXCLUDED — re-issuing them could double-apply — so they keep surfacing the
|
|
184
|
+
// bridge's honest OUTCOME-UNKNOWN error.
|
|
185
|
+
const RETRY_SAFE_CMDS = new Set([
|
|
186
|
+
// Idempotent reads (mirror UiBridge.READONLY_CMDS + list/status probes).
|
|
187
|
+
"graph_serialize",
|
|
188
|
+
"graph_outline",
|
|
189
|
+
"graph_get_errors",
|
|
190
|
+
"graph_get_subgraph",
|
|
191
|
+
"graph_prompt_director_audit",
|
|
192
|
+
"graph_query",
|
|
193
|
+
"get_todo",
|
|
194
|
+
"workflow_list",
|
|
195
|
+
"nodes_list",
|
|
196
|
+
"nodes_queue_status",
|
|
197
|
+
"node_queue_status",
|
|
198
|
+
// Idempotent full-replace UI state — re-sending the same list is a no-op (#481).
|
|
199
|
+
"set_todo",
|
|
200
|
+
]);
|
|
201
|
+
/** A command whose result is unchanged by being re-issued after a reconnect —
|
|
202
|
+
* so it is safe to transparently retry once when the transport dropped. */
|
|
203
|
+
function isRetrySafeCmd(cmd) {
|
|
204
|
+
const name = typeof cmd.cmd === "string" ? cmd.cmd : "";
|
|
205
|
+
return RETRY_SAFE_CMDS.has(name);
|
|
206
|
+
}
|
|
207
|
+
/** True when an error is a TRANSIENT transport/reconnect drop (the tab went away
|
|
208
|
+
* or was replaced), NOT a genuine command error or a live-but-frozen reply
|
|
209
|
+
* timeout. Deliberately EXCLUDES "did not reply within N ms" (a backgrounded/
|
|
210
|
+
* frozen tab — retrying just double-waits, #334) and "OUTCOME UNKNOWN" (a
|
|
211
|
+
* mutating command that may already have applied). */
|
|
212
|
+
function isTransientReconnectError(err) {
|
|
213
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
214
|
+
return /no connected tab|genuinely gone|is not open|Failed to fetch|Panel not reachable|ECONNRESET|socket hang up|premature close|other side closed|ECONNABORTED|EPIPE/i.test(msg);
|
|
215
|
+
}
|
|
216
|
+
let retrySettleMsOverride = null;
|
|
217
|
+
/** Short pause before the single post-drop retry, letting the replacement tab
|
|
218
|
+
* finish its reconnect hello so ensureReachable can resolve it. Test-overridable. */
|
|
219
|
+
function retrySettleMs() {
|
|
220
|
+
if (retrySettleMsOverride != null)
|
|
221
|
+
return retrySettleMsOverride;
|
|
222
|
+
return Math.round(parsePositiveNumberEnv("COMFYUI_PANEL_RETRY_SETTLE_S", 0.4) * 1000);
|
|
223
|
+
}
|
|
137
224
|
/**
|
|
138
225
|
* Poll the panel bridge until ComfyUI is reachable again after a reboot. Each probe
|
|
139
226
|
* is a lightweight `nodes_queue_status` round-trip (via ctx.call, which never
|
|
@@ -309,6 +396,44 @@ async function openWorkflowWithVerify(path, ctx) {
|
|
|
309
396
|
// is a REAL failure. Return the original bridge timeout error unchanged.
|
|
310
397
|
return res;
|
|
311
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Resolve a caller-supplied pin `path` (path / filename / key, any form) to the
|
|
401
|
+
* AUTHORITATIVE open-workflow record from a fresh `workflow_list` — the single
|
|
402
|
+
* source of truth for which tabs exist and their canonical `key` (#259). Returns:
|
|
403
|
+
* - the matched record when the workflow IS open (so the pin can be canonicalized
|
|
404
|
+
* to its stable key and bound to the exact frontend tab identity);
|
|
405
|
+
* - `null` when workflow_list is unreachable/empty or carries no `workflows`
|
|
406
|
+
* array (indeterminate — caller should fall back to the raw path, NOT fail);
|
|
407
|
+
* - the sentinel `NOT_OPEN` when the list IS known but the target is absent, so
|
|
408
|
+
* the caller can FAIL CLOSED instead of letting the panel silently route the
|
|
409
|
+
* pin to some other open tab.
|
|
410
|
+
*/
|
|
411
|
+
const NOT_OPEN = Symbol("workflow-not-open");
|
|
412
|
+
async function resolveOpenWorkflow(ctx, path) {
|
|
413
|
+
let parsed = null;
|
|
414
|
+
try {
|
|
415
|
+
parsed = parseToolResultJson(await ctx.call({ cmd: "workflow_list" }, 6000));
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return null; // transport error — indeterminate, don't fail the pin
|
|
419
|
+
}
|
|
420
|
+
if (!parsed)
|
|
421
|
+
return null;
|
|
422
|
+
const rawList = parsed.workflows;
|
|
423
|
+
if (!Array.isArray(rawList) || rawList.length === 0) {
|
|
424
|
+
// No enumerable tab list (older panel / stub) — can't verify, don't fail closed.
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
for (const wf of rawList) {
|
|
428
|
+
if (activeMatchesTarget(wf, path))
|
|
429
|
+
return wf;
|
|
430
|
+
}
|
|
431
|
+
// The active object is authoritative too, in case it isn't mirrored in the array.
|
|
432
|
+
if (activeMatchesTarget(parsed.active, path)) {
|
|
433
|
+
return parsed.active;
|
|
434
|
+
}
|
|
435
|
+
return NOT_OPEN;
|
|
436
|
+
}
|
|
312
437
|
export const __openWorkflowTestHooks = {
|
|
313
438
|
/** Inject fast open-verify timing so tests don't wait the real ~6s budget. */
|
|
314
439
|
setOpenVerifyTiming(timing) {
|
|
@@ -316,6 +441,7 @@ export const __openWorkflowTestHooks = {
|
|
|
316
441
|
},
|
|
317
442
|
isAckTimeout,
|
|
318
443
|
activeMatchesTarget,
|
|
444
|
+
resolveOpenWorkflow,
|
|
319
445
|
};
|
|
320
446
|
const slotRef = z.union([z.string(), z.number().int().min(0)]);
|
|
321
447
|
// CivitAI browsing-level bitmask values: PG=1, PG-13=2, R=4, X=8, XXX=16.
|
|
@@ -517,11 +643,14 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
517
643
|
// CONSERVATIVE by construction (must not weaken multi-tab routing):
|
|
518
644
|
// - fires ONLY when the current tab is genuinely unreachable (canReach false);
|
|
519
645
|
// a healthy session — including a healthy MULTI-tab one — is never touched;
|
|
520
|
-
// -
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
// `no connected tab`
|
|
646
|
+
// - STRICT-SINGLE: only silently rebinds when there is EXACTLY ONE connected
|
|
647
|
+
// tab. With 2+ live tabs the bridge's no-tabId resolution would fall back to
|
|
648
|
+
// `lastActiveTabId` — which can be an UNRELATED workflow (codex) — so the
|
|
649
|
+
// silent path refuses to guess and instead lets the command surface the
|
|
650
|
+
// bridge's clear `no connected tab` error. The user then re-binds with the
|
|
651
|
+
// EXPLICIT panel_set_workflow_target({mode:"current"}) signal, which DOES
|
|
652
|
+
// accept the last-active tab because it is a deliberate "use what's live now"
|
|
653
|
+
// consent — silent auto-heal must be stricter than an explicit rebind;
|
|
525
654
|
// - PINNED sessions are left strict: a session pinned to a specific workflow
|
|
526
655
|
// keeps requiring the explicit rebind consent signal. Only "current"-mode
|
|
527
656
|
// (follow-the-active-tab) sessions self-heal, which is faithful to what that
|
|
@@ -535,6 +664,14 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
535
664
|
return;
|
|
536
665
|
if (workflowTargets?.get(ctx.tabId)?.mode === "pinned")
|
|
537
666
|
return; // stay strict
|
|
667
|
+
// Strict-single: never silently pick among multiple live tabs (would risk the
|
|
668
|
+
// real bridge's last-active fallback routing to an unrelated workflow). When
|
|
669
|
+
// the bridge can enumerate its tabs and there is more than one, do NOT rebind.
|
|
670
|
+
if (typeof bridge.tabs === "function") {
|
|
671
|
+
const live = bridge.tabs();
|
|
672
|
+
if (Array.isArray(live) && live.length > 1)
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
538
675
|
try {
|
|
539
676
|
rebindToActiveTab();
|
|
540
677
|
}
|
|
@@ -543,14 +680,40 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
543
680
|
// command surface the bridge's own clear, tab-listing error.
|
|
544
681
|
}
|
|
545
682
|
};
|
|
683
|
+
const sendRouted = async (cmd, timeoutMs) => {
|
|
684
|
+
const target = workflowTargets?.get(ctx.tabId);
|
|
685
|
+
const routed = target ? withWorkflowTarget(cmd, target) : cmd;
|
|
686
|
+
return bridge.send(routed, { tabId: ctx.tabId, timeoutMs });
|
|
687
|
+
};
|
|
546
688
|
const call = async (cmd, timeoutMs) => {
|
|
547
689
|
try {
|
|
548
690
|
ensureReachable();
|
|
549
|
-
|
|
550
|
-
const routed = target ? withWorkflowTarget(cmd, target) : cmd;
|
|
551
|
-
return ok(await bridge.send(routed, { tabId: ctx.tabId, timeoutMs }));
|
|
691
|
+
return ok(await sendRouted(cmd, timeoutMs));
|
|
552
692
|
}
|
|
553
693
|
catch (err) {
|
|
694
|
+
// Post-reconnect retry-once: a reboot/free_vram/reconnect can drop the tab's
|
|
695
|
+
// transport (or replace it under a new tab id) the instant after we dispatch.
|
|
696
|
+
// For idempotent commands, settle briefly, rebind onto the now-live tab, and
|
|
697
|
+
// retry ONE time before surfacing an error (#278/#310/#332/#481). Mutating
|
|
698
|
+
// edits are excluded from RETRY_SAFE_CMDS, so they never double-apply.
|
|
699
|
+
if (isRetrySafeCmd(cmd) && isTransientReconnectError(err)) {
|
|
700
|
+
try {
|
|
701
|
+
await sleep(retrySettleMs());
|
|
702
|
+
ensureReachable(); // rebinds a current-mode session onto the reconnected tab
|
|
703
|
+
return ok(await sendRouted(cmd, timeoutMs));
|
|
704
|
+
}
|
|
705
|
+
catch (err2) {
|
|
706
|
+
// The retry also failed — surface an actionable reconnecting status rather
|
|
707
|
+
// than a bare transport error (#332), while still failing honestly.
|
|
708
|
+
if (isTransientReconnectError(err2)) {
|
|
709
|
+
const name = typeof cmd.cmd === "string" ? cmd.cmd : "panel command";
|
|
710
|
+
return fail(`${name} could not reach the ComfyUI panel — it is still reconnecting after a ` +
|
|
711
|
+
`restart/reload. Wait a moment and retry; if it persists, rebind with ` +
|
|
712
|
+
`panel_set_workflow_target({mode:"current"}). (${err2 instanceof Error ? err2.message : String(err2)})`);
|
|
713
|
+
}
|
|
714
|
+
return fail(err2);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
554
717
|
return fail(err);
|
|
555
718
|
}
|
|
556
719
|
};
|
|
@@ -624,7 +787,15 @@ async function resolveWorkflowInput(args, ctx) {
|
|
|
624
787
|
let reply;
|
|
625
788
|
try {
|
|
626
789
|
ctx.ensureReachable?.();
|
|
627
|
-
|
|
790
|
+
// Route to the SAME authoritative target as ctx.call: when the session is
|
|
791
|
+
// pinned, inject the pinned workflow_path so the live-canvas capture serializes
|
|
792
|
+
// the PINNED workflow, not whatever tab is visible (codex — this direct send
|
|
793
|
+
// otherwise bypasses withWorkflowTarget and reads the wrong graph).
|
|
794
|
+
const target = ctx.workflowTarget?.get(ctx.tabId);
|
|
795
|
+
const cmd = target
|
|
796
|
+
? withWorkflowTarget({ cmd: "graph_serialize" }, target)
|
|
797
|
+
: { cmd: "graph_serialize" };
|
|
798
|
+
reply = await ctx.bridge.send(cmd, {
|
|
628
799
|
tabId: ctx.tabId,
|
|
629
800
|
timeoutMs: 30000,
|
|
630
801
|
});
|
|
@@ -1075,6 +1246,17 @@ export function buildPanelToolDefs() {
|
|
|
1075
1246
|
// session is left untouched; an ambiguous multi-tab case surfaces a clear
|
|
1076
1247
|
// error rather than guessing.
|
|
1077
1248
|
if (ctx.rebindToActiveTab) {
|
|
1249
|
+
// Strict-single: if this session's tab is orphaned AND 2+ tabs are live,
|
|
1250
|
+
// do NOT guess (the bridge would fall back to last-active, possibly an
|
|
1251
|
+
// unrelated tab) — surface a clear error so the user picks, honoring the
|
|
1252
|
+
// documented "ambiguous multi-tab surfaces a clear error" promise (codex).
|
|
1253
|
+
const orphaned = typeof ctx.bridge.canReach === "function" && !ctx.bridge.canReach(ctx.tabId);
|
|
1254
|
+
const live = typeof ctx.bridge.tabs === "function" ? ctx.bridge.tabs() : undefined;
|
|
1255
|
+
if (orphaned && Array.isArray(live) && live.length > 1) {
|
|
1256
|
+
return fail("This session's ComfyUI tab was replaced and multiple tabs are now open — " +
|
|
1257
|
+
"can't safely pick one. Switch to the tab you want, then call " +
|
|
1258
|
+
'panel_set_workflow_target({mode:"current"}) before panel_reload.');
|
|
1259
|
+
}
|
|
1078
1260
|
try {
|
|
1079
1261
|
ctx.rebindToActiveTab();
|
|
1080
1262
|
}
|
|
@@ -1492,7 +1674,32 @@ export function buildPanelToolDefs() {
|
|
|
1492
1674
|
return fail(err);
|
|
1493
1675
|
}
|
|
1494
1676
|
}
|
|
1495
|
-
|
|
1677
|
+
// PIN: bind to the EXACT open-workflow identity from the authoritative
|
|
1678
|
+
// workflow_list, canonicalizing to its stable `key` and FAILING CLOSED when
|
|
1679
|
+
// the requested workflow isn't actually open — instead of letting the panel
|
|
1680
|
+
// silently route the pin to another tab (#259). Indeterminate lists (older
|
|
1681
|
+
// panel / no `workflows` array) fall back to the raw path (unchanged).
|
|
1682
|
+
let pinPath = path;
|
|
1683
|
+
let pinFilename = filename;
|
|
1684
|
+
if (mode === "pinned" && path) {
|
|
1685
|
+
const resolved = await resolveOpenWorkflow(ctx, path);
|
|
1686
|
+
if (resolved === NOT_OPEN) {
|
|
1687
|
+
return fail(`Cannot pin to "${path}" — it is not open in ComfyUI. Open it first ` +
|
|
1688
|
+
`(panel_open_workflow) or pick an open workflow from panel_list_workflows, ` +
|
|
1689
|
+
`then pin. (Refusing to pin to a workflow that isn't open so graph edits ` +
|
|
1690
|
+
`never land on the wrong tab.)`);
|
|
1691
|
+
}
|
|
1692
|
+
if (resolved) {
|
|
1693
|
+
// Canonicalize to the stable key so routing survives rename/reconnect.
|
|
1694
|
+
pinPath = resolved.key ?? resolved.path ?? path;
|
|
1695
|
+
pinFilename = filename ?? resolved.filename ?? resolved.path;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
const target = ctx.workflowTarget.set(ctx.tabId, {
|
|
1699
|
+
mode,
|
|
1700
|
+
path: pinPath,
|
|
1701
|
+
filename: pinFilename,
|
|
1702
|
+
});
|
|
1496
1703
|
ctx.bridge.push({ type: "workflow_target", target }, ctx.tabId);
|
|
1497
1704
|
const hint = target.mode === "pinned"
|
|
1498
1705
|
? `Pinned to "${target.filename ?? target.path}". Graph tools will target that workflow without switching the user's view.`
|
|
@@ -1619,7 +1826,15 @@ export function buildPanelToolDefs() {
|
|
|
1619
1826
|
})),
|
|
1620
1827
|
def("panel_screenshot", "Render the workflow the user is currently viewing (root graph, or the open subgraph) to a PNG and return it as an IMAGE so you can SEE the layout. It frames the whole graph (nodes + groups), captures, then restores the user's view. Use this to visually verify a layout you just built — overlaps, alignment, rails, colors, group bands — instead of reasoning from coordinates alone.", { padding: z.number().optional().describe("Margin around the graph in px (default 60).") }, async (args, ctx) => {
|
|
1621
1828
|
try {
|
|
1622
|
-
|
|
1829
|
+
ctx.ensureReachable?.();
|
|
1830
|
+
// Route to the same authoritative target as ctx.call: a pinned session
|
|
1831
|
+
// screenshots the PINNED workflow (via injected workflow_path), not just
|
|
1832
|
+
// whatever tab is visible (codex — graph_* must carry the pin).
|
|
1833
|
+
const target = ctx.workflowTarget?.get(ctx.tabId);
|
|
1834
|
+
const cmd = withWorkflowTarget({ cmd: "graph_screenshot", padding: args.padding }, target ?? { mode: "current" });
|
|
1835
|
+
const res = (await ctx.bridge.send(cmd, {
|
|
1836
|
+
tabId: ctx.tabId,
|
|
1837
|
+
}));
|
|
1623
1838
|
if (!res?.image)
|
|
1624
1839
|
return fail("screenshot returned no image");
|
|
1625
1840
|
return { content: [{ type: "image", data: res.image, mimeType: res.mimeType ?? "image/png" }] };
|
|
@@ -1696,6 +1911,50 @@ export function buildPanelToolDefs() {
|
|
|
1696
1911
|
const fired = rebootConfirmed(res);
|
|
1697
1912
|
const dropped = !fired && rebootDropped(res);
|
|
1698
1913
|
if (!fired && !dropped) {
|
|
1914
|
+
// The panel could not fire a Manager reboot. If the SOLE reason is that
|
|
1915
|
+
// NO Manager reboot endpoint answered (legacy Manager 3.x: v2 route 405s,
|
|
1916
|
+
// legacy route 404s — #425, panel #253/#266) AND the target is a LOCAL,
|
|
1917
|
+
// process-controllable ComfyUI, fall back to the headless managed restart
|
|
1918
|
+
// (kill + relaunch) — the same mechanism as the `restart_comfyui` tool.
|
|
1919
|
+
// A busy-guard or security refusal is deliberately NOT eligible
|
|
1920
|
+
// (rebootNoEndpoint excludes those), so this never aborts a running
|
|
1921
|
+
// render or bypasses Manager's security gate.
|
|
1922
|
+
if (!isRemoteMode() && rebootNoEndpoint(res)) {
|
|
1923
|
+
let restart;
|
|
1924
|
+
try {
|
|
1925
|
+
restart = await restartComfyUI();
|
|
1926
|
+
}
|
|
1927
|
+
catch (err) {
|
|
1928
|
+
return fail("The built-in Manager exposed no reboot endpoint (legacy Manager 3.x), " +
|
|
1929
|
+
"and the headless managed restart also failed: " +
|
|
1930
|
+
(err instanceof Error ? err.message : String(err)) +
|
|
1931
|
+
" — restart ComfyUI on the host, then reconnect.");
|
|
1932
|
+
}
|
|
1933
|
+
if (!restart.started) {
|
|
1934
|
+
return fail("The built-in Manager exposed no reboot endpoint (legacy Manager 3.x). " +
|
|
1935
|
+
"Tried the headless managed restart (kill + relaunch) as a fallback, but it " +
|
|
1936
|
+
`could not restart ComfyUI: ${restart.message} ` +
|
|
1937
|
+
"Restart ComfyUI on the host, then reconnect.");
|
|
1938
|
+
}
|
|
1939
|
+
// A managed kill+relaunch restarts ComfyUI out-of-band from the WS
|
|
1940
|
+
// client, so drop the memoized caches exactly as the Manager-reboot
|
|
1941
|
+
// path does before waiting for the panel to reconnect.
|
|
1942
|
+
resetClient();
|
|
1943
|
+
resetObjectInfoCache();
|
|
1944
|
+
const timing = getPanelRebootTiming();
|
|
1945
|
+
const recovery = await waitForPanelReady(ctx, timing);
|
|
1946
|
+
return ok({
|
|
1947
|
+
rebooting: true,
|
|
1948
|
+
ready: recovery.ready,
|
|
1949
|
+
recovered_ms: recovery.waited_ms,
|
|
1950
|
+
probes: recovery.attempts,
|
|
1951
|
+
via: "headless-managed-restart",
|
|
1952
|
+
note: "ComfyUI-Manager (legacy 3.x) had no reboot endpoint; restarted ComfyUI " +
|
|
1953
|
+
`via the headless managed restart (kill + relaunch)${recovery.ready
|
|
1954
|
+
? ` and it came back ready in ${(recovery.waited_ms / 1000).toFixed(1)}s.`
|
|
1955
|
+
: " but the panel did not reconnect within the readiness budget — verify with panel_node_queue_status."}`,
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1699
1958
|
// Genuine refusal (or an unrelated error) — do NOT poll or reset caches.
|
|
1700
1959
|
// Resetting on a refusal would close the shared client mid-generation
|
|
1701
1960
|
// (codex WS-3 finding #2).
|