comfyui-mcp 0.49.1 → 0.49.2
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/config.js +22 -0
- package/dist/config.js.map +1 -1
- package/dist/orchestrator/agent-backend.js +9 -0
- package/dist/orchestrator/agent-backend.js.map +1 -1
- package/dist/orchestrator/antigravity-backend.js +3 -2
- package/dist/orchestrator/antigravity-backend.js.map +1 -1
- package/dist/orchestrator/chatgpt-oauth-backend.js +3 -2
- package/dist/orchestrator/chatgpt-oauth-backend.js.map +1 -1
- package/dist/orchestrator/claude-backend.js +300 -3
- package/dist/orchestrator/claude-backend.js.map +1 -1
- package/dist/orchestrator/codex-backend.js +3 -2
- package/dist/orchestrator/codex-backend.js.map +1 -1
- package/dist/orchestrator/gemini-backend.js +3 -2
- package/dist/orchestrator/gemini-backend.js.map +1 -1
- package/dist/orchestrator/grok-backend.js +5 -3
- package/dist/orchestrator/grok-backend.js.map +1 -1
- package/dist/orchestrator/ollama-backend.js +3 -2
- package/dist/orchestrator/ollama-backend.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +89 -19
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +410 -6
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/orchestrator/pi-backend.js +3 -2
- package/dist/orchestrator/pi-backend.js.map +1 -1
- package/dist/services/process-control.js +95 -0
- package/dist/services/process-control.js.map +1 -1
- package/package.json +2 -2
|
@@ -40,9 +40,9 @@ import { getNsfwConsent, setNsfwConsent } from "../services/panel-settings.js";
|
|
|
40
40
|
import { QueueMonitor } from "../services/queue-monitor.js";
|
|
41
41
|
import { getClient, getObjectInfo, backfillObjectInfo, resetClient, resetObjectInfoCache, } from "../comfyui/client.js";
|
|
42
42
|
import { convertUiToApi, collectNodeTypes } from "../services/workflow-converter.js";
|
|
43
|
-
import { restartComfyUI } from "../services/process-control.js";
|
|
43
|
+
import { restartComfyUI, preflightLocalRestart, recordRestartDispatch, clearRestartDispatch, getRestartDispatchRecord, RESTART_DISPATCH_CAUSATION_WINDOW_MS, PROCESS_WIDE_RESTART_DISPATCH_TOKEN, __processControlTestHooks, } from "../services/process-control.js";
|
|
44
44
|
import { resetManagerApiCache } from "../services/manager-api-cache.js";
|
|
45
|
-
import { isRemoteMode, isCloudMode, getBootLocalComfyUIBaseUrl, getComfyUIBaseUrl, } from "../config.js";
|
|
45
|
+
import { isRemoteMode, isCloudMode, getBootLocalComfyUIBaseUrl, getComfyUIBaseUrl, getComfyuiTargetGeneration, } from "../config.js";
|
|
46
46
|
import { sliceWorkflow } from "../services/workflow-slicer.js";
|
|
47
47
|
import { validateA2UISpecServer } from "../services/a2ui-spec.js";
|
|
48
48
|
/** Treat these as an affirmative answer to a yes/no confirm card (destructive-op
|
|
@@ -206,6 +206,19 @@ const MAX_REBOOT_BUDGET_MS = 240_000; // 240s → settle+budget ≤ 250s < 300s
|
|
|
206
206
|
// fast with an actionable retry / restart_comfyui hint. It NEVER shortcuts the
|
|
207
207
|
// confirmation itself (no auto-confirm): it bounds only how long we wait for the answer.
|
|
208
208
|
const RESTART_CONFIRM_TIMEOUT_MS = 90_000; // 90s
|
|
209
|
+
// #742 decline-probe recheck window: a single ECONNREFUSED is NOT proof of a
|
|
210
|
+
// lost server — a genuinely restarting instance is refused during its normal
|
|
211
|
+
// down window (codex gate). When the decline path probes before reporting, it
|
|
212
|
+
// rechecks over this SHORT, bounded window and only declares DOWN when the
|
|
213
|
+
// endpoint is STILL refused at the end of it; a recovery inside the window is
|
|
214
|
+
// reported as such. Deliberately small: the turn already ended on the user's
|
|
215
|
+
// decline, so this is a report-time check and must not stall.
|
|
216
|
+
const DECLINE_PROBE_WINDOW_MS = 6_000; // 6s total
|
|
217
|
+
const DECLINE_PROBE_INTERVAL_MS = 2_000; // 2s between samples
|
|
218
|
+
const DECLINE_PROBE_TIMEOUT_MS = 2_000; // per-sample probe ceiling
|
|
219
|
+
// #742 r4/r5: the decline path may name restart causation only against a
|
|
220
|
+
// dispatch RECORDED within RESTART_DISPATCH_CAUSATION_WINDOW_MS (imported from
|
|
221
|
+
// process-control) whose token THIS session holds — see sessionRestartDispatch.
|
|
209
222
|
function parsePositiveNumberEnv(name, fallback) {
|
|
210
223
|
const raw = process.env[name];
|
|
211
224
|
if (raw == null || raw === "")
|
|
@@ -245,6 +258,30 @@ export const __panelToolsTestHooks = {
|
|
|
245
258
|
setHealthProbe(fn) {
|
|
246
259
|
healthProbeOverride = fn;
|
|
247
260
|
},
|
|
261
|
+
/** Inject a fake #742 restart preflight so reboot tests don't probe real
|
|
262
|
+
* processes/ports. null restores the live preflightLocalRestart. */
|
|
263
|
+
setLocalRestartPreflight(fn) {
|
|
264
|
+
localRestartPreflightOverride = fn;
|
|
265
|
+
},
|
|
266
|
+
/** Inject a fast #742 decline-probe recheck window so decline-path tests
|
|
267
|
+
* don't wait the real ~6s. null restores the DECLINE_PROBE_* constants. */
|
|
268
|
+
setDeclineProbeTiming(timing) {
|
|
269
|
+
declineProbeTimingOverride = timing;
|
|
270
|
+
},
|
|
271
|
+
/** Direct access to the #742 decline recheck loop so its hard-deadline
|
|
272
|
+
* guarantee (codex gate r2) can be unit-tested with a custom deadline. */
|
|
273
|
+
probeDeclineRecovery,
|
|
274
|
+
/** r5: the restart-dispatch record THIS session (ctx) holds, or null. */
|
|
275
|
+
getSessionRestartDispatch(ctx) {
|
|
276
|
+
return sessionRestartDispatch(ctx);
|
|
277
|
+
},
|
|
278
|
+
/** r5: seed a restart-dispatch record HELD BY this session (ctx) — with an
|
|
279
|
+
* explicit `at` so fresh/stale shapes don't need a real restart. */
|
|
280
|
+
seedSessionRestartDispatch(ctx, record) {
|
|
281
|
+
const token = randomUUID();
|
|
282
|
+
__processControlTestHooks.setRestartDispatchRecord(token, record);
|
|
283
|
+
sessionRestartDispatchTokens.set(ctx, token);
|
|
284
|
+
},
|
|
248
285
|
looksLikeSystemStats,
|
|
249
286
|
probeComfyHealth,
|
|
250
287
|
probeComfyEndpoint,
|
|
@@ -648,6 +685,66 @@ function normalizeProbe(v) {
|
|
|
648
685
|
return "down";
|
|
649
686
|
return v;
|
|
650
687
|
}
|
|
688
|
+
/**
|
|
689
|
+
* The #742 decline-path recheck: poll `base` over a short, bounded window
|
|
690
|
+
* (clamped to `deadline`) so a server that is merely mid-restart is not
|
|
691
|
+
* falsely declared lost on a single ECONNREFUSED. Samples immediately, then
|
|
692
|
+
* sleeps intervalMs between samples — "still refused at the END of the
|
|
693
|
+
* window" is the only down verdict, and ONLY when the full window ran: a
|
|
694
|
+
* deadline that truncates the window downgrades the verdict to "ambiguous"
|
|
695
|
+
* regardless of the samples taken (r3). The deadline is HARD (codex gate
|
|
696
|
+
* r2): no awaited probe may START once the remaining budget is exhausted —
|
|
697
|
+
* the loop stops and the verdict falls to the last known state (or
|
|
698
|
+
* "ambiguous" when nothing was sampled). Never throws.
|
|
699
|
+
*/
|
|
700
|
+
async function probeDeclineRecovery(base, windowMs, intervalMs, probeTimeoutMs, deadline) {
|
|
701
|
+
const start = Date.now();
|
|
702
|
+
const windowEnd = start + Math.max(1, windowMs);
|
|
703
|
+
const end = Math.min(windowEnd, deadline);
|
|
704
|
+
// r3: a window the deadline cuts short can never prove permanence — DOWN
|
|
705
|
+
// requires the FULL recheck window to have run its course.
|
|
706
|
+
const truncated = end < windowEnd;
|
|
707
|
+
const probe = healthProbeOverride ?? probeComfyEndpoint;
|
|
708
|
+
let attempts = 0;
|
|
709
|
+
let sawDown = false;
|
|
710
|
+
let last = "unknown";
|
|
711
|
+
for (;;) {
|
|
712
|
+
// HARD deadline: NEVER begin an awaited probe with an exhausted remainder
|
|
713
|
+
// (clamping an expired remainder to a 1ms timeout would still start — and
|
|
714
|
+
// await — a probe PAST the deadline, defeating the guarantee).
|
|
715
|
+
const remaining = end - Date.now();
|
|
716
|
+
if (remaining <= 0)
|
|
717
|
+
break;
|
|
718
|
+
attempts++;
|
|
719
|
+
const t = Math.min(probeTimeoutMs, remaining);
|
|
720
|
+
let status = "unknown";
|
|
721
|
+
try {
|
|
722
|
+
status = normalizeProbe(await probe(base, t));
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
status = "unknown";
|
|
726
|
+
}
|
|
727
|
+
if (status === "healthy") {
|
|
728
|
+
return {
|
|
729
|
+
status: sawDown ? "recovered" : "healthy",
|
|
730
|
+
attempts,
|
|
731
|
+
waited_ms: Date.now() - start,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
if (status === "down")
|
|
735
|
+
sawDown = true;
|
|
736
|
+
last = status;
|
|
737
|
+
const left = end - Date.now();
|
|
738
|
+
if (left <= 0)
|
|
739
|
+
break;
|
|
740
|
+
await new Promise((r) => setTimeout(r, Math.max(1, Math.min(intervalMs, left))));
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
status: last === "down" && !truncated ? "down" : "ambiguous",
|
|
744
|
+
attempts,
|
|
745
|
+
waited_ms: Date.now() - start,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
651
748
|
/**
|
|
652
749
|
* The FIXED ComfyUI base URL to health-probe during a reboot readiness wait, or
|
|
653
750
|
* null when we must fall back to the panel round-trip (as before #509). Captured by
|
|
@@ -711,6 +808,49 @@ function captureRebootHealthBase(ctx) {
|
|
|
711
808
|
return loopbackProbeUrl(base);
|
|
712
809
|
}
|
|
713
810
|
let healthProbeOverride = null;
|
|
811
|
+
/** Test injection for the #742 refuse-safe restart preflight (the real one is
|
|
812
|
+
* preflightLocalRestart in process-control). null → the live preflight. */
|
|
813
|
+
let localRestartPreflightOverride = null;
|
|
814
|
+
/** Test injection for the #742 decline-probe recheck window, so tests don't
|
|
815
|
+
* wait the real ~6s. null → the DECLINE_PROBE_* constants. */
|
|
816
|
+
let declineProbeTimingOverride = null;
|
|
817
|
+
// #742 r5: restart-dispatch tokens held PER SESSION. Each MCP session gets its
|
|
818
|
+
// own PanelToolCtx (one per connection — see the session factory in
|
|
819
|
+
// panel-mcp-http), so keying by the ctx object scopes a dispatch record to the
|
|
820
|
+
// session that dispatched it: session A's failed restart can never ground
|
|
821
|
+
// causation for session B's decline, and A's recovery clears only A's record.
|
|
822
|
+
// WeakMap → entries die with their session's ctx.
|
|
823
|
+
const sessionRestartDispatchTokens = new WeakMap();
|
|
824
|
+
/** Stamp a restart dispatch and hold its token on THIS session (replacing any
|
|
825
|
+
* record the session previously held — a session has at most one live one).
|
|
826
|
+
* Returns the held token so clears can be CLEAR-IF-SAME (r15). */
|
|
827
|
+
function stampSessionRestartDispatch(ctx, base) {
|
|
828
|
+
const prev = sessionRestartDispatchTokens.get(ctx);
|
|
829
|
+
if (prev)
|
|
830
|
+
clearRestartDispatch(prev);
|
|
831
|
+
const token = recordRestartDispatch(base);
|
|
832
|
+
sessionRestartDispatchTokens.set(ctx, token);
|
|
833
|
+
return token;
|
|
834
|
+
}
|
|
835
|
+
/** The dispatch token THIS session currently holds, or undefined. */
|
|
836
|
+
function sessionRestartDispatchToken(ctx) {
|
|
837
|
+
return sessionRestartDispatchTokens.get(ctx);
|
|
838
|
+
}
|
|
839
|
+
/** The restart-dispatch record THIS session holds, or null. */
|
|
840
|
+
function sessionRestartDispatch(ctx) {
|
|
841
|
+
const held = sessionRestartDispatchToken(ctx);
|
|
842
|
+
return held ? getRestartDispatchRecord(held) : null;
|
|
843
|
+
}
|
|
844
|
+
/** Clear ONLY when the session STILL holds `token` (r15): a newer dispatch
|
|
845
|
+
* that landed since `token` was validated keeps its record — a
|
|
846
|
+
* read-current-then-clear would evict the newer dispatch's record and make a
|
|
847
|
+
* later persistent DOWN falsely report "no restart was dispatched". */
|
|
848
|
+
function clearSessionRestartDispatchIfSame(ctx, token) {
|
|
849
|
+
if (sessionRestartDispatchTokens.get(ctx) !== token)
|
|
850
|
+
return;
|
|
851
|
+
sessionRestartDispatchTokens.delete(ctx);
|
|
852
|
+
clearRestartDispatch(token);
|
|
853
|
+
}
|
|
714
854
|
/**
|
|
715
855
|
* Observe the boot endpoint's recovery AFTER a reboot was dispatched, and certify ONLY on
|
|
716
856
|
* an OBSERVED DOWN→UP cycle. Acceptance (dispatch confirmed/dropped) is the guard against
|
|
@@ -2518,8 +2658,16 @@ allowStateFallback = false) {
|
|
|
2518
2658
|
/* fall through to the actionable error below */
|
|
2519
2659
|
}
|
|
2520
2660
|
}
|
|
2521
|
-
|
|
2522
|
-
|
|
2661
|
+
// #721: only blame an old panel when the error actually IS an
|
|
2662
|
+
// unsupported-command rejection. Anything else (e.g. the panel's graph
|
|
2663
|
+
// desync guard, whose remedy is rebinding/opening the workflow) already
|
|
2664
|
+
// carries its own remedy in the message — appending the version hint
|
|
2665
|
+
// there misdirects the agent to pack/path/graph instead.
|
|
2666
|
+
if (isPanelCmdUnsupportedError(err, "graph_serialize")) {
|
|
2667
|
+
throw new Error(`Couldn't capture the live canvas (${msg}). ` +
|
|
2668
|
+
`An older panel version may not support graph_serialize — pass pack, path, or graph instead.`);
|
|
2669
|
+
}
|
|
2670
|
+
throw new Error(`Couldn't capture the live canvas (${msg}).`);
|
|
2523
2671
|
}
|
|
2524
2672
|
const wf = reply?.workflow;
|
|
2525
2673
|
if (!wf || typeof wf !== "object") {
|
|
@@ -3027,7 +3175,7 @@ export function buildPanelToolDefs() {
|
|
|
3027
3175
|
mode: args.mode,
|
|
3028
3176
|
limit: args.limit,
|
|
3029
3177
|
})),
|
|
3030
|
-
def("panel_add_node", "Add a node to the user's OPEN ComfyUI graph by class_type (e.g. 'KSampler', 'CheckpointLoaderSimple'). The user sees it appear live; Ctrl+Z undoes it. Returns the created node's id, slots, and default widget values.", {
|
|
3178
|
+
def("panel_add_node", "Add a node to the user's OPEN ComfyUI graph by class_type (e.g. 'KSampler', 'CheckpointLoaderSimple'). The user sees it appear live; Ctrl+Z undoes it. Returns the created node's id, slots, and default widget values. Frontend-only virtual types are addable too: 'Note' and 'MarkdownNote' — the supported way to ANNOTATE a workflow with on-canvas instructions (add the node, then put the text in its 'text' widget via panel_set_widget) — plus 'Reroute' and 'PrimitiveNode'. These are LiteGraph-native and never appear in the backend node registry, so they legitimately bypass the backend class_type check.", {
|
|
3031
3179
|
class_type: z.string().describe("Exact ComfyUI node class_type to create."),
|
|
3032
3180
|
pos: xy()
|
|
3033
3181
|
.optional()
|
|
@@ -4327,7 +4475,7 @@ export function buildPanelToolDefs() {
|
|
|
4327
4475
|
return ctx.call({ cmd: "graph_update_node", id: args.id, version: args.version, channel: args.channel, mode: args.mode }, 30000);
|
|
4328
4476
|
}),
|
|
4329
4477
|
def("panel_node_queue_status", "Check the built-in Manager's install/update queue status (to see if a queued install finished). Read-only.", {}, async (_args, ctx) => ctx.call({ cmd: "nodes_queue_status" }, 20000)),
|
|
4330
|
-
def("panel_restart_comfyui", "Restart the user's ComfyUI server via the built-in Manager — needed to load newly installed/updated custom nodes. CALL THIS DIRECTLY when a restart is needed: it pops a confirm card and only restarts on a yes (don't ask separately first). ComfyUI and this agent go down briefly, then the panel auto-reconnects and you resume. ⚠️ BUSY GUARD: a restart ABORTS any in-progress or queued generation — if ComfyUI is generating, this tool REFUSES and tells you (it does NOT restart). When that happens, tell the user a render is running and WAIT for it (poll panel_node_queue_status), or pass force:true ONLY if the user explicitly confirms they want to kill the running generation. Best practice: before restarting after an install, check the queue is idle first. Only call when a restart is actually needed.", { force: z.boolean().optional() }, async ({ force }, ctx) => {
|
|
4478
|
+
def("panel_restart_comfyui", "Restart the user's ComfyUI server via the built-in Manager — needed to load newly installed/updated custom nodes. CALL THIS DIRECTLY when a restart is needed: it pops a confirm card and only restarts on a yes (don't ask separately first). ComfyUI and this agent go down briefly, then the panel auto-reconnects and you resume. ⚠️ BUSY GUARD: a restart ABORTS any in-progress or queued generation — if ComfyUI is generating, this tool REFUSES and tells you (it does NOT restart). When that happens, tell the user a render is running and WAIT for it (poll panel_node_queue_status), or pass force:true ONLY if the user explicitly confirms they want to kill the running generation. Best practice: before restarting after an install, check the queue is idle first. Only call when a restart is actually needed. On an externally-managed install whose relaunch can't be proven from here (e.g. Pinokio), the restart is REFUSED before anything is stopped — restart from the launcher that owns the server instead.", { force: z.boolean().optional() }, async ({ force }, ctx) => {
|
|
4331
4479
|
// Whole-handler budget (#536): confirm + dispatch + readiness — INCLUDING
|
|
4332
4480
|
// the legacy path's UNPREEMPTIBLE synchronous execSync blocks — must ALL finish
|
|
4333
4481
|
// under the outer ~300s tools/call limit. 255s + the legacy admission rule below
|
|
@@ -4363,6 +4511,124 @@ export function buildPanelToolDefs() {
|
|
|
4363
4511
|
"panel card.");
|
|
4364
4512
|
}
|
|
4365
4513
|
if (decision !== "yes") {
|
|
4514
|
+
// #742: NEVER claim "not restarted" while the server is actually DOWN —
|
|
4515
|
+
// and NEVER declare a loss from ONE probe (codex gate): a genuinely
|
|
4516
|
+
// restarting server is refused during its normal down window, and a
|
|
4517
|
+
// transport/card error mapping to "no" is exactly how a tab dying
|
|
4518
|
+
// during a healthy prior reboot lands here. So recheck over a short,
|
|
4519
|
+
// bounded window and report DOWN only when the endpoint is STILL
|
|
4520
|
+
// refused at the end of it; a recovery inside the window is reported
|
|
4521
|
+
// as such (bound instance) or keeps the plain cancel line (unbound).
|
|
4522
|
+
// CAUSATION ("a restart took it down") is named ONLY when the probed
|
|
4523
|
+
// target is PROVABLY the same boot instance the restart would have
|
|
4524
|
+
// stopped — the same binding the refuse-safe preflight uses; an
|
|
4525
|
+
// unbound configured endpoint gets a generic unreachable report that
|
|
4526
|
+
// never claims "we stopped it".
|
|
4527
|
+
const declineBootBase = captureRebootHealthBase(ctx);
|
|
4528
|
+
const boundToRestartTarget = declineBootBase != null && sameHttpBase(getComfyUIBaseUrl(), declineBootBase);
|
|
4529
|
+
const declineProbeBase = boundToRestartTarget
|
|
4530
|
+
? declineBootBase
|
|
4531
|
+
: getComfyUIBaseUrl();
|
|
4532
|
+
// r15: capture the session's held dispatch TOKEN before the probe
|
|
4533
|
+
// awaits — the exoneration clear (r13) and the recovery claim (r14)
|
|
4534
|
+
// must key on the token VALIDATED HERE, never whichever token is
|
|
4535
|
+
// current after the awaits: a concurrent accepted restart stamping a
|
|
4536
|
+
// fresh token mid-probe keeps its record (clear-if-same below), so a
|
|
4537
|
+
// later persistent DOWN can still attribute to it.
|
|
4538
|
+
const declineHeldToken = sessionRestartDispatchToken(ctx);
|
|
4539
|
+
const declineTiming = declineProbeTimingOverride ?? {
|
|
4540
|
+
windowMs: DECLINE_PROBE_WINDOW_MS,
|
|
4541
|
+
intervalMs: DECLINE_PROBE_INTERVAL_MS,
|
|
4542
|
+
probeTimeoutMs: DECLINE_PROBE_TIMEOUT_MS,
|
|
4543
|
+
};
|
|
4544
|
+
const outcome = await probeDeclineRecovery(declineProbeBase, declineTiming.windowMs, declineTiming.intervalMs, declineTiming.probeTimeoutMs, overallDeadline);
|
|
4545
|
+
// The VALIDATED token's record (null when it was superseded by a
|
|
4546
|
+
// newer stamp during the probe — the conservative outcome: neither
|
|
4547
|
+
// claim nor clear keys on a record this probe didn't validate).
|
|
4548
|
+
const declineHeldRecord = declineHeldToken
|
|
4549
|
+
? getRestartDispatchRecord(declineHeldToken)
|
|
4550
|
+
: null;
|
|
4551
|
+
// r13: a HEALTHY/RECOVERED observation exonerates any dispatch THIS
|
|
4552
|
+
// session has on record — the restart explained itself, so its token
|
|
4553
|
+
// must never ground causation for a LATER, independent failure.
|
|
4554
|
+
// Base-matched: a healthy observation only exonerates the instance
|
|
4555
|
+
// it was taken on (an unbound healthy endpoint says nothing about a
|
|
4556
|
+
// boot-instance record). The 10-minute causation window stays as the
|
|
4557
|
+
// backstop for records never observed back.
|
|
4558
|
+
// r14: the record is captured BEFORE the clear — the recovery CLAIM
|
|
4559
|
+
// below ("a restart initiated earlier appears to have completed")
|
|
4560
|
+
// requires the token to have been present at this moment.
|
|
4561
|
+
// r15: CLEAR-IF-SAME — the clear fires only when the session still
|
|
4562
|
+
// holds the SAME token validated before the probe; a newer dispatch
|
|
4563
|
+
// stamped mid-probe keeps its record.
|
|
4564
|
+
if (outcome.status === "healthy" || outcome.status === "recovered") {
|
|
4565
|
+
if (declineHeldToken != null &&
|
|
4566
|
+
declineHeldRecord != null &&
|
|
4567
|
+
(declineHeldRecord.base == null ||
|
|
4568
|
+
sameHttpBase(declineHeldRecord.base, declineProbeBase))) {
|
|
4569
|
+
clearSessionRestartDispatchIfSame(ctx, declineHeldToken);
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
if (outcome.status === "down") {
|
|
4573
|
+
const secs = Math.max(1, Math.round(outcome.waited_ms / 1000));
|
|
4574
|
+
if (boundToRestartTarget) {
|
|
4575
|
+
// r4: causation may be named ONLY against a RECORDED restart
|
|
4576
|
+
// dispatch — recent enough to plausibly be the cause, and
|
|
4577
|
+
// targeting THIS instance. r5: the record must be one THIS
|
|
4578
|
+
// SESSION dispatched (holds the token of) — another session's
|
|
4579
|
+
// failed restart never grounds causation here. An unrelated
|
|
4580
|
+
// crash / manual stop (no record, a stale record, another
|
|
4581
|
+
// instance's record, or another session's record) gets the
|
|
4582
|
+
// causation-free report.
|
|
4583
|
+
const dispatch = sessionRestartDispatch(ctx);
|
|
4584
|
+
const causative = dispatch != null &&
|
|
4585
|
+
Date.now() - dispatch.at <= RESTART_DISPATCH_CAUSATION_WINDOW_MS &&
|
|
4586
|
+
(dispatch.base == null || sameHttpBase(dispatch.base, declineBootBase));
|
|
4587
|
+
if (causative) {
|
|
4588
|
+
return ok("⚠️ ComfyUI is DOWN — it was STOPPED and did not come back (still " +
|
|
4589
|
+
`unreachable after a ${secs}s recheck window), so do NOT treat this as ` +
|
|
4590
|
+
"\"nothing happened\". The restart just declined was NOT what stopped " +
|
|
4591
|
+
"it (nothing was dispatched after the decline), but a restart initiated " +
|
|
4592
|
+
"earlier already took the server down and it never returned. Start " +
|
|
4593
|
+
"ComfyUI manually from whatever launches it (e.g. Pinokio, the Desktop " +
|
|
4594
|
+
"app, or your terminal), then reload the panel tab so it reconnects. " +
|
|
4595
|
+
"restart_comfyui can attempt the relaunch for you when the install's " +
|
|
4596
|
+
"launch path is resolvable.");
|
|
4597
|
+
}
|
|
4598
|
+
return ok("⚠️ ComfyUI is DOWN — still unreachable after " +
|
|
4599
|
+
`a ${secs}s recheck window — and no restart was dispatched through me ` +
|
|
4600
|
+
"that would explain it (the restart just declined was NOT dispatched, " +
|
|
4601
|
+
"and none is on record recently for this instance). Something else " +
|
|
4602
|
+
"stopped it (a crash, a manual stop, or its launcher). Start ComfyUI " +
|
|
4603
|
+
"manually from whatever launches it (e.g. Pinokio, the Desktop app, or " +
|
|
4604
|
+
"your terminal), then reload the panel tab so it reconnects.");
|
|
4605
|
+
}
|
|
4606
|
+
return ok("⚠️ ComfyUI appears to be DOWN — the configured endpoint was still " +
|
|
4607
|
+
`unreachable after a ${secs}s recheck window. The restart just declined ` +
|
|
4608
|
+
"was NOT dispatched, and I can't confirm from here whether the server " +
|
|
4609
|
+
"was actually stopped — this endpoint isn't provably the instance the " +
|
|
4610
|
+
"restart would have cycled. Check ComfyUI on its host and start it " +
|
|
4611
|
+
"manually if it is down, then reload the panel tab so it reconnects.");
|
|
4612
|
+
}
|
|
4613
|
+
if (outcome.status === "recovered" && boundToRestartTarget) {
|
|
4614
|
+
// r14: the recovery CLAIM ("a restart initiated earlier appears to
|
|
4615
|
+
// have completed") passes the SAME causation gate as the DOWN
|
|
4616
|
+
// report — a session-held, bound-confirmed record, recent, and
|
|
4617
|
+
// base-matched, captured BEFORE the r13 exoneration clear (which
|
|
4618
|
+
// fires either way). Without it, the down→healthy cycle alone
|
|
4619
|
+
// proves nothing about what caused the down: report the recovery
|
|
4620
|
+
// causation-free.
|
|
4621
|
+
const recoveryCausative = declineHeldRecord != null &&
|
|
4622
|
+
Date.now() - declineHeldRecord.at <= RESTART_DISPATCH_CAUSATION_WINDOW_MS &&
|
|
4623
|
+
(declineHeldRecord.base == null ||
|
|
4624
|
+
sameHttpBase(declineHeldRecord.base, declineBootBase));
|
|
4625
|
+
return ok(recoveryCausative
|
|
4626
|
+
? "Cancelled — no new restart was dispatched. Note: ComfyUI was briefly " +
|
|
4627
|
+
"unreachable but is healthy again — a restart initiated earlier " +
|
|
4628
|
+
"appears to have completed."
|
|
4629
|
+
: "Cancelled — no new restart was dispatched. ComfyUI was briefly " +
|
|
4630
|
+
"unreachable but is healthy again.");
|
|
4631
|
+
}
|
|
4366
4632
|
return ok("Cancelled — ComfyUI was not restarted.");
|
|
4367
4633
|
}
|
|
4368
4634
|
// Heal an orphaned session onto the live tab FIRST, then bind the reboot dispatch
|
|
@@ -4371,6 +4637,99 @@ export function buildPanelToolDefs() {
|
|
|
4371
4637
|
// server-authorized + immutable, bound to the exact host FAMILY the reboot goes
|
|
4372
4638
|
// to (null unless the bound tab provably fronts our boot instance).
|
|
4373
4639
|
ctx.ensureReachable?.();
|
|
4640
|
+
// #742 REFUSE-SAFE PREFLIGHT: a Manager reboot stops ComfyUI OUT-OF-BAND —
|
|
4641
|
+
// it never goes through our validated kill+relaunch — so before dispatching
|
|
4642
|
+
// anything, the stop must be provable survivable (#368/#370: losing a restart
|
|
4643
|
+
// is cheap, losing the server is not). On a Pinokio-style install (externally
|
|
4644
|
+
// supervised; no main.py/interpreter resolvable from here) a plain Manager
|
|
4645
|
+
// restart kills the process and the supervisor does NOT re-launch it — the
|
|
4646
|
+
// exact #742 lost-server. When the reboot would target OUR local boot
|
|
4647
|
+
// instance (the same instance binding the legacy fallback uses), prove a
|
|
4648
|
+
// relaunch is possible FIRST and refuse BEFORE any stop when it isn't. Only
|
|
4649
|
+
// the PROVEN-dangerous shape refuses (a reachable local non-Desktop process
|
|
4650
|
+
// with an unbuildable/unvalidatable relaunch); every other shape — Desktop
|
|
4651
|
+
// (Electron-supervised, #400), unverifiable, or remote — proceeds exactly as
|
|
4652
|
+
// before. The binding for this DECISION is captured pre-await; nothing
|
|
4653
|
+
// downstream may reuse it (r7).
|
|
4654
|
+
const preflightHealthBase = captureRebootHealthBase(ctx);
|
|
4655
|
+
if (preflightHealthBase != null && sameHttpBase(getComfyUIBaseUrl(), preflightHealthBase)) {
|
|
4656
|
+
// Snapshot the target GENERATION at the decision (r11): a final-state
|
|
4657
|
+
// base comparison (A vs A) cannot detect an intervening A→B→A
|
|
4658
|
+
// retarget, so stability is judged by the monotonic epoch bumped on
|
|
4659
|
+
// EVERY retarget — any mutation, including a round trip back to the
|
|
4660
|
+
// same base, is caught.
|
|
4661
|
+
const preflightTargetGeneration = getComfyuiTargetGeneration();
|
|
4662
|
+
const preflight = await (localRestartPreflightOverride ?? preflightLocalRestart)();
|
|
4663
|
+
// r8/r9/r10: the preflight AWAIT makes the pre-decision captures
|
|
4664
|
+
// STALE — and the preflight itself reads MUTABLE config (target URL,
|
|
4665
|
+
// port, COMFYUI_PATH) throughout, so a config retarget during the
|
|
4666
|
+
// await can re-point the whole assessment at a DIFFERENT install
|
|
4667
|
+
// while the tab still fronts the original one. Re-heal and
|
|
4668
|
+
// re-capture BEFORE trusting the result either way. The guarantee
|
|
4669
|
+
// that must hold: a stop/reboot is only ever sent when the preflight
|
|
4670
|
+
// validated THE instance the dispatch will cycle.
|
|
4671
|
+
ctx.ensureReachable?.();
|
|
4672
|
+
const postPreflightHealthBase = captureRebootHealthBase(ctx);
|
|
4673
|
+
const tabFrontsSameInstance = postPreflightHealthBase != null &&
|
|
4674
|
+
sameHttpBase(preflightHealthBase, postPreflightHealthBase);
|
|
4675
|
+
const configStable = getComfyuiTargetGeneration() === preflightTargetGeneration;
|
|
4676
|
+
if (!configStable && tabFrontsSameInstance) {
|
|
4677
|
+
// r10: the target config moved MID-CHECK, so the preflight result
|
|
4678
|
+
// — pass OR fail — cannot vouch for the tab-fronted instance (a
|
|
4679
|
+
// PASS may have validated a different, safe install; it must never
|
|
4680
|
+
// bless a stop of this one). Its relaunch is UNPROVEN → refuse; an
|
|
4681
|
+
// instance with an unproven relaunch is never sent a stop.
|
|
4682
|
+
return ok({
|
|
4683
|
+
rebooting: false,
|
|
4684
|
+
ready: false,
|
|
4685
|
+
confirmed_cycle: false,
|
|
4686
|
+
refused: true,
|
|
4687
|
+
note: "Refusing to restart ComfyUI: the ComfyUI target configuration changed " +
|
|
4688
|
+
"while the restart safety check was running, so the check cannot vouch " +
|
|
4689
|
+
"for a safe relaunch of the instance this panel fronts. A stop is never " +
|
|
4690
|
+
"sent to an instance whose relaunch is unproven — ComfyUI was NOT " +
|
|
4691
|
+
"stopped (it is still running). Let the target settle, then retry " +
|
|
4692
|
+
"panel_restart_comfyui.",
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
if (!preflight.ok && tabFrontsSameInstance) {
|
|
4696
|
+
// r9: the danger proof follows the INSTANCE the tab fronts, NOT the
|
|
4697
|
+
// mutable runtime config — a config-only retarget mid-await must
|
|
4698
|
+
// not wash out the proof that the tab-fronted boot instance is
|
|
4699
|
+
// unrelaunchable. The tab STILL fronts that same instance, so the
|
|
4700
|
+
// proof is still valid and the refusal stands: an instance proven
|
|
4701
|
+
// unrelaunchable is NEVER sent a stop/reboot, regardless of
|
|
4702
|
+
// config/tab shuffling mid-flight. Only a genuinely different,
|
|
4703
|
+
// unconfirmable target falls through to the honest-unconfirmed
|
|
4704
|
+
// dispatch (nothing was ever proved dangerous for it — and nothing
|
|
4705
|
+
// was ever stopped, the preflight never stops anything).
|
|
4706
|
+
return ok({
|
|
4707
|
+
rebooting: false,
|
|
4708
|
+
ready: false,
|
|
4709
|
+
confirmed_cycle: false,
|
|
4710
|
+
refused: true,
|
|
4711
|
+
note: `Refusing to restart ComfyUI: ${preflight.reason} This looks like an ` +
|
|
4712
|
+
"externally-managed install (e.g. Pinokio): a restart from here would STOP " +
|
|
4713
|
+
"ComfyUI and nothing would bring it back automatically, so it was refused " +
|
|
4714
|
+
"BEFORE anything was stopped — ComfyUI is still running. Restart it from the " +
|
|
4715
|
+
"launcher that owns it (e.g. Pinokio's own controls), or point COMFYUI_PATH " +
|
|
4716
|
+
"at the live install so a relaunch can be proven and use restart_comfyui.",
|
|
4717
|
+
});
|
|
4718
|
+
}
|
|
4719
|
+
// Otherwise: a PASS with a stable config (proven safe for THE
|
|
4720
|
+
// tab-fronted instance — proceed), or the tab now fronts a genuinely
|
|
4721
|
+
// different, unconfirmable target (nothing provable about it — the
|
|
4722
|
+
// dispatch path below treats that honestly, r6/r7). Nothing was
|
|
4723
|
+
// stopped in any case — the preflight never stops anything.
|
|
4724
|
+
}
|
|
4725
|
+
// r7: the preflight AWAIT sits between the binding capture and the dispatch,
|
|
4726
|
+
// breaking the no-await invariant above — a tab/connection rebind during
|
|
4727
|
+
// that await would make every pre-await capture stale (the dispatch would
|
|
4728
|
+
// go to an unconfirmable/different target while the causation stamp, the
|
|
4729
|
+
// recovery observer, and the legacy-fallback gate all read the STALE bound
|
|
4730
|
+
// base). Re-heal and re-capture the tab id, panel identity, and health base
|
|
4731
|
+
// AT THE DISPATCH POINT; everything below uses ONLY these fresh captures.
|
|
4732
|
+
ctx.ensureReachable?.();
|
|
4374
4733
|
const boundTabId = ctx.tabId;
|
|
4375
4734
|
// Snapshot the exact browser-tab registration that is about to receive the
|
|
4376
4735
|
// reboot. A post-restart success must observe a strictly newer hello from
|
|
@@ -4539,6 +4898,18 @@ export function buildPanelToolDefs() {
|
|
|
4539
4898
|
" — restart ComfyUI on the host, then reconnect.");
|
|
4540
4899
|
}
|
|
4541
4900
|
clearTimeout(restartTimer);
|
|
4901
|
+
// #742 r5/r6: the managed restart stopped the process — record the
|
|
4902
|
+
// dispatch with THIS session holding the token, stamped with the
|
|
4903
|
+
// BOUND-CONFIRMED base (this fallback only runs when the instance
|
|
4904
|
+
// binding held, so healthBase is non-null here). restartComfyUI
|
|
4905
|
+
// also stamped its own process-wide record, which never grounds
|
|
4906
|
+
// causation. Only a PROVEN stop is recorded; a refusal/timeout
|
|
4907
|
+
// (restart undefined, or stopped!==true) records nothing. The
|
|
4908
|
+
// token is kept so the recovery clear below is CLEAR-IF-SAME (r15).
|
|
4909
|
+
let legacyDispatchToken;
|
|
4910
|
+
if (restart?.stopped === true) {
|
|
4911
|
+
legacyDispatchToken = stampSessionRestartDispatch(ctx, healthBase);
|
|
4912
|
+
}
|
|
4542
4913
|
// DEFINITIVE no-restart: a spawn failure, OR restartComfyUI refused before
|
|
4543
4914
|
// stopping anything (no process found / unsafe relaunch → stopped:false &&
|
|
4544
4915
|
// started:false). The process was NOT cycled, so the still-healthy endpoint is
|
|
@@ -4554,6 +4925,14 @@ export function buildPanelToolDefs() {
|
|
|
4554
4925
|
// Otherwise (the process WAS stopped/started, or restartComfyUI's own readiness
|
|
4555
4926
|
// poll merely expired — neither terminal) DEFER to OUR OWN observed DOWN→UP.
|
|
4556
4927
|
const recovery = await proofPromise;
|
|
4928
|
+
// #742 r4/r5/r15: the managed restart was observed back — clear THIS
|
|
4929
|
+
// session's record, CLEAR-IF-SAME: only when the session still holds
|
|
4930
|
+
// the token THIS restart stamped (a concurrent dispatch's newer
|
|
4931
|
+
// record survives). restartComfyUI also clears its own process-wide
|
|
4932
|
+
// record on success; this covers only-observer-saw-it recoveries.
|
|
4933
|
+
if (recovery.ready && legacyDispatchToken != null) {
|
|
4934
|
+
clearSessionRestartDispatchIfSame(ctx, legacyDispatchToken);
|
|
4935
|
+
}
|
|
4557
4936
|
const observed = recovery.via === "observed-cycle";
|
|
4558
4937
|
// The legacy Manager path restarts ComfyUI out-of-band too. Server recovery alone
|
|
4559
4938
|
// is not graph-tool readiness: wait for the browser tab to reconnect, then verify
|
|
@@ -4608,6 +4987,25 @@ export function buildPanelToolDefs() {
|
|
|
4608
4987
|
resetClient();
|
|
4609
4988
|
resetObjectInfoCache();
|
|
4610
4989
|
resetManagerApiCache("panel Manager reboot");
|
|
4990
|
+
// #742 r4/r5/r6: record the ACTUAL dispatch (acceptance proven — a refusal
|
|
4991
|
+
// never reaches here). ONLY a BOUND-CONFIRMED target (the same binding
|
|
4992
|
+
// the r1 causation scoping and the refuse-safe preflight use) may stamp a
|
|
4993
|
+
// causation-capable record — held on THIS session with the BOUND base at
|
|
4994
|
+
// stamp time, never the mutable configured one. An unbound/unconfirmable
|
|
4995
|
+
// target (r6) stamps only the shared PROCESS-WIDE slot, which never
|
|
4996
|
+
// grounds causation: the dispatch can't be proven to have hit the
|
|
4997
|
+
// instance a later decline would probe, and a session that rebinds to
|
|
4998
|
+
// the boot tab afterward can't claim it either (a re-targeted session
|
|
4999
|
+
// can't prove the earlier dispatch hit its current instance — no claim
|
|
5000
|
+
// is the truthful answer). It is cleared below if observed back —
|
|
5001
|
+
// CLEAR-IF-SAME on the token stamped here (r15).
|
|
5002
|
+
let acceptedDispatchToken;
|
|
5003
|
+
if (healthBase != null && sameHttpBase(getComfyUIBaseUrl(), healthBase)) {
|
|
5004
|
+
acceptedDispatchToken = stampSessionRestartDispatch(ctx, healthBase);
|
|
5005
|
+
}
|
|
5006
|
+
else {
|
|
5007
|
+
recordRestartDispatch(getComfyUIBaseUrl(), PROCESS_WIDE_RESTART_DISPATCH_TOKEN);
|
|
5008
|
+
}
|
|
4611
5009
|
// Observe recovery. There is exactly ONE sound proof that THIS ComfyUI instance
|
|
4612
5010
|
// actually cycled: a directly OBSERVED down→up on the server-authorized, immutable,
|
|
4613
5011
|
// family-bound boot endpoint (observeRecovery). We do NOT fabricate a second proof
|
|
@@ -4642,6 +5040,12 @@ export function buildPanelToolDefs() {
|
|
|
4642
5040
|
// down→up, which the observer has been (and continues) watching for.
|
|
4643
5041
|
gate.deadline = Math.min(Date.now() + timing.budgetMs, overallDeadline);
|
|
4644
5042
|
const recovery = await recoveryPromise;
|
|
5043
|
+
// #742 r4/r5/r15: the dispatched restart was observed back — clear the
|
|
5044
|
+
// record, CLEAR-IF-SAME: only when the session still holds the token
|
|
5045
|
+
// THIS dispatch stamped (a concurrent dispatch's newer record survives).
|
|
5046
|
+
if (recovery.ready && acceptedDispatchToken != null) {
|
|
5047
|
+
clearSessionRestartDispatchIfSame(ctx, acceptedDispatchToken);
|
|
5048
|
+
}
|
|
4645
5049
|
if (!recovery.ready) {
|
|
4646
5050
|
const waited = Math.round(recovery.waited_ms / 1000);
|
|
4647
5051
|
return ok({
|