comfyui-mcp 0.52.4 → 0.52.6
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/orchestrator/agent-backend.js.map +1 -1
- package/dist/orchestrator/claude-backend.js +10 -2
- package/dist/orchestrator/claude-backend.js.map +1 -1
- package/dist/orchestrator/index.js +125 -3
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/ollama-backend.js +53 -3
- package/dist/orchestrator/ollama-backend.js.map +1 -1
- package/dist/orchestrator/open-identity-normalization.js +267 -0
- package/dist/orchestrator/open-identity-normalization.js.map +1 -0
- package/dist/orchestrator/panel-agent.js +52 -5
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +347 -29
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/orchestrator/pi-backend.js +45 -16
- package/dist/orchestrator/pi-backend.js.map +1 -1
- package/dist/orchestrator/session-store.js +17 -1
- package/dist/orchestrator/session-store.js.map +1 -1
- package/dist/services/agent-identity.js +175 -0
- package/dist/services/agent-identity.js.map +1 -0
- package/dist/services/civitai-resolver.js +10 -0
- package/dist/services/civitai-resolver.js.map +1 -1
- package/dist/services/code-provider-auth.js +13 -0
- package/dist/services/code-provider-auth.js.map +1 -1
- package/dist/services/download-cache.js +48 -4
- package/dist/services/download-cache.js.map +1 -1
- package/dist/services/env-capabilities.js +1 -0
- package/dist/services/env-capabilities.js.map +1 -1
- package/dist/services/launch-argv.js +82 -0
- package/dist/services/launch-argv.js.map +1 -0
- package/dist/services/launcher-env.js +94 -0
- package/dist/services/launcher-env.js.map +1 -1
- package/dist/services/manifest-partial.js +85 -0
- package/dist/services/manifest-partial.js.map +1 -0
- package/dist/services/manifest.js +28 -16
- package/dist/services/manifest.js.map +1 -1
- package/dist/services/node-authoring.js +9 -10
- package/dist/services/node-authoring.js.map +1 -1
- package/dist/services/node-dev.js +32 -25
- package/dist/services/node-dev.js.map +1 -1
- package/dist/services/node-verify.js +8 -7
- package/dist/services/node-verify.js.map +1 -1
- package/dist/services/openai-provider-registry.js +12 -0
- package/dist/services/openai-provider-registry.js.map +1 -1
- package/dist/services/output-dir.js +6 -68
- package/dist/services/output-dir.js.map +1 -1
- package/dist/services/process-control.js +39 -7
- package/dist/services/process-control.js.map +1 -1
- package/dist/services/ui-bridge.js +84 -0
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/services/workspace-env.js +53 -25
- package/dist/services/workspace-env.js.map +1 -1
- package/dist/tools/manifest.js +1 -1
- package/dist/tools/manifest.js.map +1 -1
- package/dist/tools/node-pack.js +27 -16
- package/dist/tools/node-pack.js.map +1 -1
- package/dist/tools/report-issue.js +74 -4
- package/dist/tools/report-issue.js.map +1 -1
- package/dist/tools/report-issue.test.js +154 -2
- package/dist/tools/report-issue.test.js.map +1 -1
- package/locales/ar/main.json +2 -0
- package/locales/en/main.json +2 -0
- package/locales/es/main.json +2 -0
- package/locales/fa/main.json +2 -0
- package/locales/fr/main.json +2 -0
- package/locales/ja/main.json +2 -0
- package/locales/ko/main.json +2 -0
- package/locales/pt-BR/main.json +2 -0
- package/locales/ru/main.json +2 -0
- package/locales/tr/main.json +2 -0
- package/locales/zh/main.json +2 -0
- package/locales/zh-TW/main.json +2 -0
- package/package.json +1 -1
- package/packs/ltx-2.3-xy-plot/pack.yaml +14 -3
- package/packs/ltx-2.3-xy-plot/workflow.json +6 -6
- package/packs/z-image-turbo-controlnet/install-runpod.sh +0 -2
- package/packs/z-image-turbo-controlnet/install-windows.bat +0 -2
- package/packs/z-image-turbo-controlnet/manifest.yaml +7 -5
- package/packs/z-image-turbo-controlnet/pack.yaml +3 -2
- package/packs/z-image-turbo-controlnet/workflow.json +3 -139
- package/packs/z-image-xy-plot/install-runpod.sh +1 -1
- package/packs/z-image-xy-plot/install-windows.bat +1 -1
- package/packs/z-image-xy-plot/manifest.yaml +18 -14
- package/packs/z-image-xy-plot/pack.yaml +17 -5
- package/packs/z-image-xy-plot/workflow.json +12 -12
- package/plugin/skills/lora-manager/SKILL.md +59 -0
- package/scripts/arena-baseline.mjs +20 -0
- package/scripts/arena-graphic.mjs +4 -1
- package/scripts/arena-report.mjs +105 -0
- package/scripts/check-pack-models.mjs +94 -3
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// gating for panel_clear/panel_restart_comfyui) is IDENTICAL across providers,
|
|
24
24
|
// so parity is automatic — neither path reimplements a tool.
|
|
25
25
|
import { z } from "zod";
|
|
26
|
-
import { randomUUID } from "node:crypto";
|
|
26
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
27
27
|
// #873 — the operator's tool-surface policy also governs the panel surface.
|
|
28
28
|
import { resolveToolSurfacePolicy, toolActionPolicyError, toolAllowed, } from "../tools/tool-surface-filter.js";
|
|
29
29
|
import { logger as toolPolicyLogger } from "../utils/logger.js";
|
|
@@ -34,6 +34,7 @@ import { fileURLToPath } from "node:url";
|
|
|
34
34
|
import { comfyuiFetch } from "../comfyui/fetch.js";
|
|
35
35
|
import { assertPanelNotTargetedUnverifiable } from "../services/panel-pin-guard.js";
|
|
36
36
|
import { nodesInstallCommandArgs } from "../services/node-management.js";
|
|
37
|
+
import { formatQueueStatusPartialNote, getManifestPartialLeftover, } from "../services/manifest-partial.js";
|
|
37
38
|
import { searchPanelNodes } from "../services/manager-node-search.js";
|
|
38
39
|
import { isPanelAnsweredError } from "../services/panel-answered.js";
|
|
39
40
|
import { isPreExecutorRefusal } from "../services/panel-refusal.js";
|
|
@@ -45,6 +46,7 @@ import { primePanelBase, verifiedPanelDiskVersion, } from "../services/panel-wor
|
|
|
45
46
|
import { conversationOfScopeAddress, isScopeAddress, shortTabId } from "../services/session-scope.js";
|
|
46
47
|
import { NODE_ID_MESSAGE, NODE_ID_PATTERN, normalizeNodeId } from "./node-id.js";
|
|
47
48
|
import { parseContradictoryPromotedWidgetRefusal, resolveInnerPromotedTarget, } from "./promoted-widget.js";
|
|
49
|
+
import { isPlainObject, isStampMismatchSaveRefusal, patchOpenIdentity, shouldRebindOpenIdentity, workflowFromSerializeReply, } from "./open-identity-normalization.js";
|
|
48
50
|
import { clearSwitchHold, describeSwitchHold, recordSwitchHold, successProvesSwitchCleared, } from "./switch-hold.js";
|
|
49
51
|
import { NO_ORIGIN_REMEDY } from "./fence-refusal.js";
|
|
50
52
|
/** #884 — journal TICKETS (run completions #468, ask answers #486) must be
|
|
@@ -94,6 +96,13 @@ import { config, isRemoteMode, isCloudMode, getBootLocalComfyUIBaseUrl, getComfy
|
|
|
94
96
|
import { normalizeInstallPathEnv } from "../utils/install-path-env.js";
|
|
95
97
|
import { sliceWorkflow } from "../services/workflow-slicer.js";
|
|
96
98
|
import { validateA2UISpecServer } from "../services/a2ui-spec.js";
|
|
99
|
+
/** #1700 — orchestrator wires this so panel_reload can fork-respawn the agent
|
|
100
|
+
* with a freshly-read MCP set. Unset in tests that only exercise the tool. */
|
|
101
|
+
let applyMcpReload;
|
|
102
|
+
/** Bind (or clear) the in-process MCP-config respawn used by panel_reload. */
|
|
103
|
+
export function setApplyMcpReload(fn) {
|
|
104
|
+
applyMcpReload = fn;
|
|
105
|
+
}
|
|
97
106
|
/** Treat these as an affirmative answer to a yes/no confirm card (destructive-op
|
|
98
107
|
* gate). Deliberately BROAD/lenient — a false "no" only SKIPS a destructive op, so
|
|
99
108
|
* erring toward "not yes" is safe. NEVER use this for the adult-consent gate: a
|
|
@@ -844,6 +853,17 @@ function isWorkflowInstanceMismatch(err) {
|
|
|
844
853
|
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
845
854
|
return /workflow instance mismatch/i.test(msg);
|
|
846
855
|
}
|
|
856
|
+
/** Would the next graph command still resolve `ctx.tabId` to a live tab? */
|
|
857
|
+
function sessionRouteIsLive(ctx) {
|
|
858
|
+
if (typeof ctx.bridge.canReach !== "function")
|
|
859
|
+
return true;
|
|
860
|
+
try {
|
|
861
|
+
return ctx.bridge.canReach(ctx.tabId) === true;
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
847
867
|
/**
|
|
848
868
|
* #1331 — the bridge refused a mutation because the active workflow has no identity to
|
|
849
869
|
* fence against (ui-bridge's `no trusted identity` branch).
|
|
@@ -2342,6 +2362,35 @@ async function settleDroppedEnqueue(ctx, res, dispatch) {
|
|
|
2342
2362
|
`when the Manager will not take it. This tool cannot clone for you — it drives whatever ` +
|
|
2343
2363
|
`ComfyUI the panel is bound to, which need not be this machine.`);
|
|
2344
2364
|
}
|
|
2365
|
+
/**
|
|
2366
|
+
* #1699 — a drained Manager queue is not apply_manifest completion when
|
|
2367
|
+
* custom_nodes were never submitted. Keep the panel payload parseable (do not
|
|
2368
|
+
* append prose after the JSON) and name the leftover entries on the object.
|
|
2369
|
+
*/
|
|
2370
|
+
function annotateQueueStatusWithManifestPartial(res) {
|
|
2371
|
+
const leftover = getManifestPartialLeftover();
|
|
2372
|
+
if (!leftover || leftover.not_started.length === 0)
|
|
2373
|
+
return res;
|
|
2374
|
+
if (res.isError)
|
|
2375
|
+
return res;
|
|
2376
|
+
const parsed = parseToolResultJson(res);
|
|
2377
|
+
if (!parsed)
|
|
2378
|
+
return appendNote(res, formatQueueStatusPartialNote(leftover));
|
|
2379
|
+
return {
|
|
2380
|
+
...res,
|
|
2381
|
+
content: [
|
|
2382
|
+
{
|
|
2383
|
+
type: "text",
|
|
2384
|
+
text: JSON.stringify({
|
|
2385
|
+
...parsed,
|
|
2386
|
+
apply_manifest_partial: leftover,
|
|
2387
|
+
queue_complete_for_apply_manifest: false,
|
|
2388
|
+
}, null, 2),
|
|
2389
|
+
},
|
|
2390
|
+
...res.content.slice(1),
|
|
2391
|
+
],
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2345
2394
|
/** Parse a ctx.call ToolResult's text payload as JSON, or null if not parseable. */
|
|
2346
2395
|
function parseToolResultJson(res) {
|
|
2347
2396
|
if (!res || res.isError)
|
|
@@ -2386,6 +2435,31 @@ function isStaleNodeSchemaRefusal(res) {
|
|
|
2386
2435
|
return false;
|
|
2387
2436
|
return /added or retyped since this page loaded its node schema/i.test(textOfToolResult(res));
|
|
2388
2437
|
}
|
|
2438
|
+
/**
|
|
2439
|
+
* #1708 — the panel's add-node guard polls `app.widgets` for a constructor. ComfyUI
|
|
2440
|
+
* 1.49+ puts `getCustomWidgets()` results in the Pinia widget store, so
|
|
2441
|
+
* AUTOCOMPLETE_TEXT_* (LoRA Manager Vue widgets) never appears there. The 5s wait
|
|
2442
|
+
* always expires and the refusal names a reload that cannot help.
|
|
2443
|
+
*/
|
|
2444
|
+
function isLoraManagerAutocompleteRefusal(res) {
|
|
2445
|
+
if (!res.isError)
|
|
2446
|
+
return false;
|
|
2447
|
+
const text = textOfToolResult(res);
|
|
2448
|
+
return (/had no widget after .+ waiting for node extensions to register/i.test(text) &&
|
|
2449
|
+
/AUTOCOMPLETE_TEXT_/i.test(text));
|
|
2450
|
+
}
|
|
2451
|
+
const LORA_MANAGER_AUTOCOMPLETE_NOTE = `\n\nThis is NOT a missing pack and NOT a widget that is still loading. ` +
|
|
2452
|
+
`ComfyUI-LoRA-Manager registers AUTOCOMPLETE_TEXT_* via getCustomWidgets() into ` +
|
|
2453
|
+
`the frontend widget store; the add-node guard only looks at app.widgets (core types), ` +
|
|
2454
|
+
`so reload / panel_refresh_nodes / retry will keep failing. ` +
|
|
2455
|
+
`Add "LoRA Text Loader (LoraManager)" instead — same MODEL/CLIP/trigger_words outputs, ` +
|
|
2456
|
+
`but its lora_syntax input is a standard STRING socket you can drive with panel_set_widget ` +
|
|
2457
|
+
`or a wired string. Core LoraLoader also works. Do not retry this class_type.`;
|
|
2458
|
+
function withLoraManagerAutocompleteNote(res) {
|
|
2459
|
+
if (!isLoraManagerAutocompleteRefusal(res))
|
|
2460
|
+
return res;
|
|
2461
|
+
return appendToolResultText(res, LORA_MANAGER_AUTOCOMPLETE_NOTE);
|
|
2462
|
+
}
|
|
2389
2463
|
function appendToolResultText(res, extra) {
|
|
2390
2464
|
const idx = res?.content?.findIndex((c) => c.type === "text") ?? -1;
|
|
2391
2465
|
const block = idx >= 0 ? res.content[idx] : undefined;
|
|
@@ -2395,6 +2469,62 @@ function appendToolResultText(res, extra) {
|
|
|
2395
2469
|
content[idx] = { type: "text", text: `${block.text}${extra}` };
|
|
2396
2470
|
return { ...res, content };
|
|
2397
2471
|
}
|
|
2472
|
+
// ---- #1695: bound the panel_set_widget previous/new echo --------------------
|
|
2473
|
+
//
|
|
2474
|
+
// Code-mode clients (Codex `functions.exec`) batch several panel_set_widget
|
|
2475
|
+
// calls into one script, and the OUTER tool result accumulates every nested
|
|
2476
|
+
// reply. Each reply echoes the previous and new widget values verbatim; two
|
|
2477
|
+
// long CLIPTextEncode prompts per call push the aggregate past the client's
|
|
2478
|
+
// tool-result budget ("Output exceeded the available model context and was
|
|
2479
|
+
// truncated") even though every mutation succeeded — and the truncation can
|
|
2480
|
+
// hide exactly the per-call status the caller needs. The echo is confirmation,
|
|
2481
|
+
// not transport (a read-back belongs to panel_query_graph), so a long string
|
|
2482
|
+
// value is summarized server-side as { chars, sha256, preview }: the hash keeps
|
|
2483
|
+
// the confirmation verifiable, the preview keeps it legible, and `echo: "full"`
|
|
2484
|
+
// opts back into the verbatim reply.
|
|
2485
|
+
//
|
|
2486
|
+
// Server-side on purpose — the echo is produced by the panel frontend, and an
|
|
2487
|
+
// old panel keeps echoing verbatim no matter what a new one would do (the same
|
|
2488
|
+
// reason fitQueryGraphReply re-fits here, #807).
|
|
2489
|
+
const SET_WIDGET_ECHO_SUMMARY_THRESHOLD = 1000;
|
|
2490
|
+
const SET_WIDGET_ECHO_PREVIEW_CHARS = 160;
|
|
2491
|
+
function summarizeSetWidgetEcho(res, full) {
|
|
2492
|
+
if (full || res.isError)
|
|
2493
|
+
return res;
|
|
2494
|
+
const payload = parseToolResultJson(res);
|
|
2495
|
+
const set = payload?.set;
|
|
2496
|
+
if (!payload || !set || typeof set !== "object" || Array.isArray(set))
|
|
2497
|
+
return res;
|
|
2498
|
+
const record = set;
|
|
2499
|
+
const summarize = (v) => {
|
|
2500
|
+
if (typeof v !== "string" || v.length <= SET_WIDGET_ECHO_SUMMARY_THRESHOLD)
|
|
2501
|
+
return v;
|
|
2502
|
+
return {
|
|
2503
|
+
chars: v.length,
|
|
2504
|
+
sha256: createHash("sha256").update(v, "utf8").digest("hex"),
|
|
2505
|
+
preview: `${v.slice(0, SET_WIDGET_ECHO_PREVIEW_CHARS)}…`,
|
|
2506
|
+
};
|
|
2507
|
+
};
|
|
2508
|
+
const previous = summarize(record.previous);
|
|
2509
|
+
const value = summarize(record.value);
|
|
2510
|
+
// Nothing crossed the threshold — return the reply untouched rather than
|
|
2511
|
+
// dressing up an unfitted payload as a fitted one.
|
|
2512
|
+
if (previous === record.previous && value === record.value)
|
|
2513
|
+
return res;
|
|
2514
|
+
const idx = res.content.findIndex((c) => c.type === "text");
|
|
2515
|
+
if (idx < 0)
|
|
2516
|
+
return res;
|
|
2517
|
+
const fitted = {
|
|
2518
|
+
...payload,
|
|
2519
|
+
set: { ...record, previous, value },
|
|
2520
|
+
echo: "summary",
|
|
2521
|
+
echo_full: 'Long string values were summarized as {chars, sha256, preview} so batched calls do not overflow the outer tool result; the mutation was applied. Re-call with echo: "full" for the verbatim strings, or read the live value back with panel_query_graph.',
|
|
2522
|
+
};
|
|
2523
|
+
return {
|
|
2524
|
+
...res,
|
|
2525
|
+
content: res.content.map((c, i) => i === idx && c.type === "text" ? { ...c, text: JSON.stringify(fitted, null, 2) } : c),
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2398
2528
|
// ---- #809: turn the panel's silent `truncated: true` booleans into a remedy --------
|
|
2399
2529
|
//
|
|
2400
2530
|
// A bare boolean is the WORST truncation signal there is: it is a field, not prose, so
|
|
@@ -3586,6 +3716,79 @@ function identityClaimedContentUnverifiedNote(detail) {
|
|
|
3586
3716
|
`expecting the opened file. Retry the graph read (panel_graph_outline) once the tab answers.` +
|
|
3587
3717
|
busy);
|
|
3588
3718
|
}
|
|
3719
|
+
/**
|
|
3720
|
+
* #1710 — restamp extra.comfyui_mcp onto the dest the tab is already bound to,
|
|
3721
|
+
* and copy dest-file promoted widgets onto any live empty slots.
|
|
3722
|
+
*
|
|
3723
|
+
* Fail-closed: dest file unreadable, node-set mismatch, or a non-empty live
|
|
3724
|
+
* widget that disagrees with dest (#1639 previous graph) leaves extra alone.
|
|
3725
|
+
*/
|
|
3726
|
+
async function tryRebindConfirmedOpenIdentity(ctx, destPath) {
|
|
3727
|
+
let live = null;
|
|
3728
|
+
try {
|
|
3729
|
+
const serialized = await ctx.call({ cmd: "graph_serialize" }, 8000);
|
|
3730
|
+
live = workflowFromSerializeReply(parseToolResultJson(serialized));
|
|
3731
|
+
}
|
|
3732
|
+
catch {
|
|
3733
|
+
return { status: "skipped" };
|
|
3734
|
+
}
|
|
3735
|
+
if (!live)
|
|
3736
|
+
return { status: "skipped" };
|
|
3737
|
+
let dest;
|
|
3738
|
+
try {
|
|
3739
|
+
dest = await readWorkflowFromPath(destPath);
|
|
3740
|
+
}
|
|
3741
|
+
catch {
|
|
3742
|
+
return { status: "skipped" };
|
|
3743
|
+
}
|
|
3744
|
+
if (!shouldRebindOpenIdentity({ live, dest, destPath }))
|
|
3745
|
+
return { status: "skipped" };
|
|
3746
|
+
const destUuid = await activeWorkflowUuidForPath(ctx, destPath);
|
|
3747
|
+
const { graph, restoredWidgets } = patchOpenIdentity(live, dest, destPath, destUuid);
|
|
3748
|
+
const loaded = await ctx.call({ cmd: "graph_load", graph }, 30000);
|
|
3749
|
+
if (loaded.isError)
|
|
3750
|
+
return { status: "skipped" };
|
|
3751
|
+
if (destUuid)
|
|
3752
|
+
refreshWorkflowUuid(ctx, { workflow_uuid: destUuid });
|
|
3753
|
+
return { status: "rebound", destPath, destUuid, restoredWidgets };
|
|
3754
|
+
}
|
|
3755
|
+
async function activeWorkflowUuidForPath(ctx, destPath) {
|
|
3756
|
+
try {
|
|
3757
|
+
const list = parseToolResultJson(await ctx.call({ cmd: "workflow_list" }, 6000));
|
|
3758
|
+
if (!list)
|
|
3759
|
+
return undefined;
|
|
3760
|
+
if (readOpenActiveAgainstTarget(list.active, destPath, list.active_confirmed) !== "same") {
|
|
3761
|
+
return undefined;
|
|
3762
|
+
}
|
|
3763
|
+
return responseWorkflowUuid(list.active);
|
|
3764
|
+
}
|
|
3765
|
+
catch {
|
|
3766
|
+
return undefined;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
async function activeSavedPath(ctx) {
|
|
3770
|
+
try {
|
|
3771
|
+
const list = parseToolResultJson(await ctx.call({ cmd: "workflow_list" }, 6000));
|
|
3772
|
+
const path = isPlainObject(list?.active) ? list.active.path : undefined;
|
|
3773
|
+
return typeof path === "string" && path ? path : undefined;
|
|
3774
|
+
}
|
|
3775
|
+
catch {
|
|
3776
|
+
return undefined;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
function openedIdentityReboundResult(destPath, rebound) {
|
|
3780
|
+
return ok({
|
|
3781
|
+
opened: { path: destPath },
|
|
3782
|
+
identity_rebound: true,
|
|
3783
|
+
restored_promoted_widgets: rebound.restoredWidgets,
|
|
3784
|
+
note: `Opened ${destPath}. Frontend-normalized presentation/widget fields differed after load; ` +
|
|
3785
|
+
`extra.comfyui_mcp.workflow_path/workflow_uuid were rebound to this workflow so a later ` +
|
|
3786
|
+
`save will not refuse a source-stamp mismatch.` +
|
|
3787
|
+
(rebound.restoredWidgets > 0
|
|
3788
|
+
? ` Restored ${rebound.restoredWidgets} empty promoted widget value(s) from the saved graph.`
|
|
3789
|
+
: ""),
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3589
3792
|
function identityClaimedButLiveGraphUnchangedNote(ctx) {
|
|
3590
3793
|
const fence = currentWorkflowFence(ctx);
|
|
3591
3794
|
const fenceTxt = fence.known && fence.uuid
|
|
@@ -3600,7 +3803,7 @@ function identityClaimedButLiveGraphUnchangedNote(ctx) {
|
|
|
3600
3803
|
`Read the graph (panel_graph_outline) to see what is actually open, then retry ` +
|
|
3601
3804
|
`panel_open_workflow or panel_load_workflow if the canvas did not switch.`);
|
|
3602
3805
|
}
|
|
3603
|
-
async function clearFenceOnIdentityProvenOpen(ctx, res) {
|
|
3806
|
+
async function clearFenceOnIdentityProvenOpen(ctx, res, requestedPath) {
|
|
3604
3807
|
const text = toolResultText(res);
|
|
3605
3808
|
// ONLY the class that states identity was proven. The UNPROVEN verdict ("could not
|
|
3606
3809
|
// prove that the active canvas was rebound") must keep failing closed: re-deriving a
|
|
@@ -3609,6 +3812,15 @@ async function clearFenceOnIdentityProvenOpen(ctx, res) {
|
|
|
3609
3812
|
return { res, repaired: false };
|
|
3610
3813
|
const canvas = await probeLiveGraphUnderCurrentFence(ctx);
|
|
3611
3814
|
if (canvas.status === "answered") {
|
|
3815
|
+
// #1710 — the old stamp still answering is ALSO what a save-as copy looks
|
|
3816
|
+
// like: dest tab, dest nodes, extra.comfyui_mcp still naming the source.
|
|
3817
|
+
// Confirm dest file content before touching extra (#1639 stays fail-closed).
|
|
3818
|
+
if (requestedPath) {
|
|
3819
|
+
const rebound = await tryRebindConfirmedOpenIdentity(ctx, requestedPath);
|
|
3820
|
+
if (rebound.status === "rebound") {
|
|
3821
|
+
return { res: openedIdentityReboundResult(requestedPath, rebound), repaired: true };
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3612
3824
|
return { res: appendToolResultText(res, identityClaimedButLiveGraphUnchangedNote(ctx)), repaired: false };
|
|
3613
3825
|
}
|
|
3614
3826
|
if (canvas.status === "unanswered") {
|
|
@@ -3793,6 +4005,20 @@ function responseWorkflowUuid(value) {
|
|
|
3793
4005
|
return WORKFLOW_UUID_RE.test(raw) ? raw : undefined;
|
|
3794
4006
|
}
|
|
3795
4007
|
/** Refresh only the bridge-owned command stamp, never caller data. */
|
|
4008
|
+
/** #1656 — tell the orchestrator that a live read confirmed the routed tab's CURRENT
|
|
4009
|
+
* stamp, so a value inherited on a same-socket re-hello stops being treated as
|
|
4010
|
+
* inherited. Never throws, and can never move a fence (see the bridge method). */
|
|
4011
|
+
function corroborateTabStamp(ctx, workflowUuid) {
|
|
4012
|
+
const fn = ctx.bridge.corroborateTabStamp;
|
|
4013
|
+
if (typeof fn !== "function")
|
|
4014
|
+
return false;
|
|
4015
|
+
try {
|
|
4016
|
+
return fn.call(ctx.bridge, ctx.tabId, workflowUuid) === true;
|
|
4017
|
+
}
|
|
4018
|
+
catch {
|
|
4019
|
+
return false;
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
3796
4022
|
function refreshWorkflowUuid(ctx, value) {
|
|
3797
4023
|
const uuid = responseWorkflowUuid(value);
|
|
3798
4024
|
const refresh = ctx.bridge.refreshWorkflowUuid;
|
|
@@ -4295,8 +4521,29 @@ async function rebindWorkflowFence(ctx, opts) {
|
|
|
4295
4521
|
// "already current" requires a KNOWN prior fence equal to the live uuid. An
|
|
4296
4522
|
// UNKNOWN prior fence must not short-circuit the adoption: we would be claiming
|
|
4297
4523
|
// the stamp already matched without ever having read it.
|
|
4298
|
-
if (before.known && before.uuid === uuid)
|
|
4524
|
+
if (before.known && before.uuid === uuid) {
|
|
4525
|
+
// #1656 — THE FENCE DOES NOT MOVE HERE, BUT THE EVIDENCE DOES.
|
|
4526
|
+
//
|
|
4527
|
+
// Reaching this line means the panel's own corroborated workflow_list says the
|
|
4528
|
+
// LIVE canvas is this very instance. If the routed tab's stamp was one CARRIED
|
|
4529
|
+
// onto a new tab id by a same-socket re-hello that could not resolve an identity,
|
|
4530
|
+
// that carried value has now been confirmed by an observation of the tab under its
|
|
4531
|
+
// CURRENT id — so the dispatch-time agreement gate must stop treating it as
|
|
4532
|
+
// inherited. Without this, a rename/save whose hello merely RACED the canvas
|
|
4533
|
+
// identity would leave the session refusing edits to a canvas the panel agrees is
|
|
4534
|
+
// the right one: the fix would be worse than the bug.
|
|
4535
|
+
//
|
|
4536
|
+
// This is NOT the retarget #1646 removed, and deliberately not `refreshWorkflowUuid`
|
|
4537
|
+
// either — that one WRITES a stamp (and a conversation's issue-time stamp with it).
|
|
4538
|
+
// `corroborateTabStamp` can only promote the provenance of a uuid the orchestrator
|
|
4539
|
+
// has itself confirmed already equals the tab's current stamp; it writes no stamp and
|
|
4540
|
+
// refuses outright when the values differ, so no target changes and no later edit is
|
|
4541
|
+
// re-aimed. The uuid still goes through the orchestrator's identity validator, never
|
|
4542
|
+
// off parsed prose, and the result is ignored: this is corroboration, and a diagnostic
|
|
4543
|
+
// must never replace the outcome it is describing.
|
|
4544
|
+
corroborateTabStamp(ctx, uuid);
|
|
4299
4545
|
return { status: "already_current", uuid, before };
|
|
4546
|
+
}
|
|
4300
4547
|
// #1646 — a READ-ONLY probe never moves the fence: the live canvas naming a
|
|
4301
4548
|
// DIFFERENT workflow is reported, not adopted. Only a deliberate rebind
|
|
4302
4549
|
// (panel_set_workflow_target, open/new) may replace the fence — a mismatch
|
|
@@ -5051,7 +5298,7 @@ async function openWorkflowWithVerify(path, ctx) {
|
|
|
5051
5298
|
return res;
|
|
5052
5299
|
const probe = await observeActiveAfterOpen(ctx, path);
|
|
5053
5300
|
if (probe.status !== "different") {
|
|
5054
|
-
const cleared = await clearFenceOnIdentityProvenOpen(ctx, res);
|
|
5301
|
+
const cleared = await clearFenceOnIdentityProvenOpen(ctx, res, path);
|
|
5055
5302
|
// #1560 — the probe's SILENCE, reported. Suppressed when the fence was in fact
|
|
5056
5303
|
// repaired: that re-derivation is itself a `workflow_list` round trip, so a
|
|
5057
5304
|
// success there proves the channel answered and the note would contradict the
|
|
@@ -8788,7 +9035,7 @@ export function buildPanelToolDefs() {
|
|
|
8788
9035
|
},
|
|
8789
9036
|
},
|
|
8790
9037
|
])),
|
|
8791
|
-
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. ADD NODES ONE AT A TIME, not as a parallel batch: each add carries a fresh /object_info payload and those register SERIALLY, so N concurrent adds become N sequential refresh cycles. On a large install that outruns the 30s per-command deadline and the later adds time out WHILE STILL QUEUED — they then apply when their turn arrives, leaving nodes you were told had failed (panel#767). Sequential adds each get the refresh to themselves and stay well inside the deadline.", {
|
|
9038
|
+
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. ComfyUI-LoRA-Manager's 'Lora Loader (LoraManager)' (and other AUTOCOMPLETE_TEXT_* nodes) cannot be added: the pack is healthy, but the add-node guard cannot see its Vue autocomplete widget. Use 'LoRA Text Loader (LoraManager)' — same outputs, lora_syntax is a STRING — or core LoraLoader; reload/retry will not clear it. ADD NODES ONE AT A TIME, not as a parallel batch: each add carries a fresh /object_info payload and those register SERIALLY, so N concurrent adds become N sequential refresh cycles. On a large install that outruns the 30s per-command deadline and the later adds time out WHILE STILL QUEUED — they then apply when their turn arrives, leaving nodes you were told had failed (panel#767). Sequential adds each get the refresh to themselves and stay well inside the deadline.", {
|
|
8792
9039
|
class_type: z.string().describe("Exact ComfyUI node class_type to create."),
|
|
8793
9040
|
pos: xy()
|
|
8794
9041
|
.optional()
|
|
@@ -8802,7 +9049,7 @@ export function buildPanelToolDefs() {
|
|
|
8802
9049
|
const add = () => ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
|
|
8803
9050
|
const first = await add();
|
|
8804
9051
|
if (!isStaleNodeSchemaRefusal(first))
|
|
8805
|
-
return first;
|
|
9052
|
+
return withLoraManagerAutocompleteNote(first);
|
|
8806
9053
|
// #1329 — DO THE REFRESH THE REFUSAL ASKS FOR, instead of billing the caller.
|
|
8807
9054
|
//
|
|
8808
9055
|
// The reporter uploaded two images, and both LoadImage adds were refused with
|
|
@@ -8820,19 +9067,19 @@ export function buildPanelToolDefs() {
|
|
|
8820
9067
|
// The refusal is the better error: it names what is wrong with the SCHEMA and
|
|
8821
9068
|
// what clears it. Carry the refresh failure alongside so the caller knows the
|
|
8822
9069
|
// automatic attempt happened and why it did not help.
|
|
8823
|
-
return appendToolResultText(first, `\n\n(Tried to clear this automatically: panel_refresh_nodes was dispatched and FAILED, ` +
|
|
9070
|
+
return withLoraManagerAutocompleteNote(appendToolResultText(first, `\n\n(Tried to clear this automatically: panel_refresh_nodes was dispatched and FAILED, ` +
|
|
8824
9071
|
`so the schema is unchanged and retrying the add will refuse again. ` +
|
|
8825
|
-
`${textOfToolResult(refreshed)})`);
|
|
9072
|
+
`${textOfToolResult(refreshed)})`));
|
|
8826
9073
|
}
|
|
8827
9074
|
const second = await add();
|
|
8828
9075
|
if (!isStaleNodeSchemaRefusal(second))
|
|
8829
|
-
return second;
|
|
9076
|
+
return withLoraManagerAutocompleteNote(second);
|
|
8830
9077
|
// Still stale after a successful refresh: report THAT, because it means the
|
|
8831
9078
|
// remedy the refusal prescribes does not fix this instance and a caller
|
|
8832
9079
|
// following it by hand would loop.
|
|
8833
|
-
return appendToolResultText(second, `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
|
|
9080
|
+
return withLoraManagerAutocompleteNote(appendToolResultText(second, `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
|
|
8834
9081
|
`and the add still refuses, so repeating panel_refresh_nodes will not clear it. ` +
|
|
8835
|
-
`Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`);
|
|
9082
|
+
`Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`));
|
|
8836
9083
|
}),
|
|
8837
9084
|
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_node", node_id: args.node_id })),
|
|
8838
9085
|
def("panel_clear", "Remove EVERY node from the user's open graph — only for an explicit 'clear/reset the canvas'. Just CALL THIS DIRECTLY when they ask to clear: the tool itself pops a confirm card and only wipes on a yes (don't ask separately first). The wipe is a single Ctrl+Z undo. NEVER use this for a 'new workflow' — that's panel_new_workflow (a new tab, leaves this graph intact).", {}, async (_args, ctx) => {
|
|
@@ -9227,7 +9474,7 @@ export function buildPanelToolDefs() {
|
|
|
9227
9474
|
node_id: nodeId().describe("Node id whose input to disconnect."),
|
|
9228
9475
|
input: slotRef.optional().describe("Input slot name or index (default 0)."),
|
|
9229
9476
|
}, async (args, ctx) => ctx.call({ cmd: "graph_disconnect", node_id: args.node_id, input: args.input })),
|
|
9230
|
-
def("panel_set_widget", "Set a widget value on a node in the user's open graph (steps, cfg, seed, ckpt_name, text prompts, …). Returns the previous and new value. Undoable with Ctrl+Z. To CLEAR a text widget to an empty string, pass `clear: true` (some MCP clients drop an empty-string `value` from the serialized payload, so `value: \"\"` may not arrive — `clear: true` always works). For the LTXDirector timeline node (WhatDreamsCost CSGlide), set `timeline_data` with the FULL timeline JSON (segments + global_prompt) to drive its custom timeline UI — this re-syncs the editor and regenerates its derived `local_prompts`/`segment_lengths`/`guide_strength` widgets; setting those derived widgets directly is refused (they are silently reverted).", {
|
|
9477
|
+
def("panel_set_widget", "Set a widget value on a node in the user's open graph (steps, cfg, seed, ckpt_name, text prompts, …). Returns the previous and new value (a string longer than 1000 chars is echoed as {chars, sha256, preview} so batched calls stay inside the outer tool-result budget — pass `echo: \"full\"` for the verbatim string). Undoable with Ctrl+Z. To CLEAR a text widget to an empty string, pass `clear: true` (some MCP clients drop an empty-string `value` from the serialized payload, so `value: \"\"` may not arrive — `clear: true` always works). For the LTXDirector timeline node (WhatDreamsCost CSGlide), set `timeline_data` with the FULL timeline JSON (segments + global_prompt) to drive its custom timeline UI — this re-syncs the editor and regenerates its derived `local_prompts`/`segment_lengths`/`guide_strength` widgets; setting those derived widgets directly is refused (they are silently reverted).", {
|
|
9231
9478
|
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
9232
9479
|
widget: z.string().describe("Widget name (e.g. 'steps', 'cfg', 'text')."),
|
|
9233
9480
|
value: z
|
|
@@ -9238,6 +9485,10 @@ export function buildPanelToolDefs() {
|
|
|
9238
9485
|
.boolean()
|
|
9239
9486
|
.optional()
|
|
9240
9487
|
.describe("Set true to clear the widget to an empty string (\"\"). Escape hatch for when a client cannot carry an empty-string `value` through tool-arg JSON. Overrides `value`."),
|
|
9488
|
+
echo: z
|
|
9489
|
+
.enum(["summary", "full"])
|
|
9490
|
+
.optional()
|
|
9491
|
+
.describe("How to echo the previous/new values back. 'summary' (default) replaces a string longer than 1000 chars with {chars, sha256, preview} — the mutation is applied either way. 'full' returns the verbatim strings."),
|
|
9241
9492
|
}, async (args, ctx) => {
|
|
9242
9493
|
// Distinguish "value present but empty" from "value absent" by key
|
|
9243
9494
|
// presence, NOT a truthiness check — an empty string is a legitimate
|
|
@@ -9251,7 +9502,14 @@ export function buildPanelToolDefs() {
|
|
|
9251
9502
|
// a single revalidation, #338/#458) — that authoritative fetch can outlast
|
|
9252
9503
|
// the 6000 ms default ack on a large install and return a FALSE timeout.
|
|
9253
9504
|
// Give the guarded write the bounded refresh ack budget.
|
|
9254
|
-
|
|
9505
|
+
// #1695: summarize the previous/new echo of every successful write
|
|
9506
|
+
// (direct, remapped, or inner-subgraph) before anything else touches
|
|
9507
|
+
// it — long prompts batched in one code-mode script otherwise overflow
|
|
9508
|
+
// the outer tool result even though the mutations succeeded. Applied
|
|
9509
|
+
// to the raw success reply so a later appendToolResultText disclosure
|
|
9510
|
+
// (which makes the text no longer parse as JSON) cannot dodge it.
|
|
9511
|
+
const echoFull = args.echo === "full";
|
|
9512
|
+
const write = async (nodeId, widget) => summarizeSetWidgetEcho(await ctx.call({ cmd: "graph_set_widget", node_id: nodeId, widget, value }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS), echoFull);
|
|
9255
9513
|
const first = await write(args.node_id, args.widget);
|
|
9256
9514
|
if (!first.isError)
|
|
9257
9515
|
return first;
|
|
@@ -9871,6 +10129,13 @@ export function buildPanelToolDefs() {
|
|
|
9871
10129
|
}
|
|
9872
10130
|
}
|
|
9873
10131
|
const scope = args.scope ?? "orchestrator";
|
|
10132
|
+
// #1700 — the frontend soft_reload only drops the websocket (this pack's
|
|
10133
|
+
// /reload is a 503). The live agent keeps the MCP set it was spawned
|
|
10134
|
+
// with, and a plain resume restores the session-recorded set anyway.
|
|
10135
|
+
// Schedule a forked in-process respawn so makeMcpServers() re-reads
|
|
10136
|
+
// ~/.claude.json and the replacement session is NOT the old MCP set.
|
|
10137
|
+
if (scope === "orchestrator")
|
|
10138
|
+
applyMcpReload?.(ctx.tabId);
|
|
9874
10139
|
const res = await ctx.call({ cmd: "soft_reload", scope }, 15000);
|
|
9875
10140
|
// #765 — the agent reads this result as its last context before the
|
|
9876
10141
|
// reload, and again after the resume, so it is the place to state what
|
|
@@ -10599,9 +10864,23 @@ export function buildPanelToolDefs() {
|
|
|
10599
10864
|
if (ctx.awaitReachable && !(await ctx.awaitReachable())) {
|
|
10600
10865
|
return noReachableTabFail(args.name ? "workflow_save_as" : "workflow_save", ctx);
|
|
10601
10866
|
}
|
|
10602
|
-
const
|
|
10603
|
-
?
|
|
10604
|
-
:
|
|
10867
|
+
const saveOnce = () => args.name
|
|
10868
|
+
? ctx.call({ cmd: "workflow_save_as", name: args.name }, 15000)
|
|
10869
|
+
: ctx.call({ cmd: "workflow_save" }, 15000);
|
|
10870
|
+
let res = await saveOnce();
|
|
10871
|
+
// #1710 — dest tab + dest nodes, extra still stamped as the save-as SOURCE.
|
|
10872
|
+
// Rebind extra to the confirmed dest, then retry the same save once.
|
|
10873
|
+
if (res.isError && isStampMismatchSaveRefusal(toolResultText(res))) {
|
|
10874
|
+
const destPath = await activeSavedPath(ctx);
|
|
10875
|
+
if (destPath) {
|
|
10876
|
+
const rebound = await tryRebindConfirmedOpenIdentity(ctx, destPath);
|
|
10877
|
+
if (rebound.status === "rebound") {
|
|
10878
|
+
const retried = await saveOnce();
|
|
10879
|
+
if (!retried.isError)
|
|
10880
|
+
res = retried;
|
|
10881
|
+
}
|
|
10882
|
+
}
|
|
10883
|
+
}
|
|
10605
10884
|
if (res.isError)
|
|
10606
10885
|
return res;
|
|
10607
10886
|
// #1045 — a SAVE-AS replaces the active workflow instance: the canvas is
|
|
@@ -11040,19 +11319,50 @@ export function buildPanelToolDefs() {
|
|
|
11040
11319
|
// known-negative write case is stated rather than papered over.
|
|
11041
11320
|
let probeRefused;
|
|
11042
11321
|
let probeOk = false;
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11322
|
+
const probeOnce = async () => {
|
|
11323
|
+
probeRefused = undefined;
|
|
11324
|
+
probeOk = false;
|
|
11325
|
+
try {
|
|
11326
|
+
// The cheapest fenced read there is: ids only, one row. It is refused by the
|
|
11327
|
+
// same instance fence every graph command carries, which is exactly the
|
|
11328
|
+
// question — a full outline would answer it no better and would cost the caller
|
|
11329
|
+
// a page of graph on a recovery path.
|
|
11330
|
+
const probe = await ctx.call({ cmd: "graph_query", fields: "ids", limit: 1 }, 8000);
|
|
11331
|
+
if (!probe.isError)
|
|
11332
|
+
probeOk = true;
|
|
11333
|
+
else
|
|
11334
|
+
probeRefused = isWorkflowInstanceMismatch(toolResultText(probe));
|
|
11335
|
+
}
|
|
11336
|
+
catch (err) {
|
|
11337
|
+
probeRefused = isWorkflowInstanceMismatch(err);
|
|
11338
|
+
}
|
|
11339
|
+
};
|
|
11340
|
+
await probeOnce();
|
|
11341
|
+
// #1703 — a passing graph_query is not evidence the NEXT graph command
|
|
11342
|
+
// will route. After a restart the scope pin can still name the OLD
|
|
11343
|
+
// wf:<route>:<path> while the reconnect has already hellod under a new
|
|
11344
|
+
// one; ensureReachable will not move a scope-bound ctx, so claiming
|
|
11345
|
+
// "no recovery needed" stranded the following panel_graph_outline with
|
|
11346
|
+
// "no connected tab". Confirm the live route; if it is dead, take the
|
|
11347
|
+
// same explicit rebind the caller would get from repeating this tool,
|
|
11348
|
+
// and only then re-probe on the tab the next command will use.
|
|
11349
|
+
if (probeOk && !sessionRouteIsLive(ctx)) {
|
|
11350
|
+
try {
|
|
11351
|
+
ctx.rebindToActiveTab?.({ scopeRecoveryConsent: true });
|
|
11352
|
+
}
|
|
11353
|
+
catch {
|
|
11354
|
+
// a failed rebind is not a fence verdict
|
|
11355
|
+
}
|
|
11356
|
+
if (sessionRouteIsLive(ctx))
|
|
11357
|
+
await probeOnce();
|
|
11358
|
+
else {
|
|
11359
|
+
probeOk = false;
|
|
11360
|
+
probeRefused = undefined;
|
|
11361
|
+
}
|
|
11053
11362
|
}
|
|
11054
|
-
|
|
11055
|
-
|
|
11363
|
+
if (probeOk && !sessionRouteIsLive(ctx)) {
|
|
11364
|
+
probeOk = false;
|
|
11365
|
+
probeRefused = undefined;
|
|
11056
11366
|
}
|
|
11057
11367
|
if (probeOk) {
|
|
11058
11368
|
probePassed = true;
|
|
@@ -11476,7 +11786,15 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
11476
11786
|
assertPanelNotTargetedUnverifiable("panel_update_node", args.id);
|
|
11477
11787
|
return ctx.call({ cmd: "graph_update_node", id: args.id, version: args.version, channel: args.channel, mode: args.mode }, 30000);
|
|
11478
11788
|
}),
|
|
11479
|
-
def("panel_node_queue_status", "Check the built-in Manager's install/update queue status (to see if a queued install finished). Read-only.
|
|
11789
|
+
def("panel_node_queue_status", "Check the built-in Manager's install/update queue status (to see if a queued install finished). Read-only. " +
|
|
11790
|
+
"A drained queue (total_count: 0, is_processing: false) only means THIS queue is idle — " +
|
|
11791
|
+
"it does not include apply_manifest custom_nodes that were never submitted (#1699). " +
|
|
11792
|
+
"When apply_manifest returned a PARTIAL INSTALL, this tool names the leftover entries and " +
|
|
11793
|
+
"sets queue_complete_for_apply_manifest:false; do not restart ComfyUI until those are " +
|
|
11794
|
+
"submitted (re-run apply_manifest).", {}, async (_args, ctx) => {
|
|
11795
|
+
const res = await ctx.call({ cmd: "nodes_queue_status" }, 20000);
|
|
11796
|
+
return annotateQueueStatusWithManifestPartial(res);
|
|
11797
|
+
}),
|
|
11480
11798
|
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. If a crash takes the panel bridge offline so the confirmation card cannot be shown, this tool falls back to a headless restart of the configured local process (or COMFYUI_RESTART_COMMAND) instead of depending on the dead bridge — it still refuses a readable busy queue without force:true, and still refuses when a relaunch cannot be proven. 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, or set COMFYUI_RESTART_COMMAND to the exact command that restarts the instance (e.g. `docker restart <container>`): the restart then runs through that command (the busy guard above still applies) instead of needing the launch path.", { force: z.boolean().optional() }, async ({ force }, ctx) => {
|
|
11481
11799
|
// Whole-handler budget (#536): confirm + dispatch + readiness — INCLUDING
|
|
11482
11800
|
// the legacy path's UNPREEMPTIBLE synchronous execSync blocks — must ALL finish
|