comfyui-mcp 0.52.4 → 0.52.7
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 +409 -31
- 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/skill-generator.js +27 -0
- package/dist/services/skill-generator.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/README.md +2 -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/agents/explorer.md +2 -0
- package/plugin/commands/node-skill.md +1 -1
- package/plugin/skills/ai-toolkit-trainer/SKILL.md +5 -0
- package/plugin/skills/anima-base/SKILL.md +5 -0
- package/plugin/skills/anima-lora-trainer/SKILL.md +5 -0
- package/plugin/skills/civitai/SKILL.md +5 -0
- package/plugin/skills/color-correction/SKILL.md +5 -0
- package/plugin/skills/comfyui-core/SKILL.md +5 -0
- package/plugin/skills/comfyui-frontend-extensions/SKILL.md +8 -7
- package/plugin/skills/comfyui-launch-flags/SKILL.md +2 -5
- package/plugin/skills/comfyui-node-registry/SKILL.md +5 -0
- package/plugin/skills/debug-render/SKILL.md +5 -0
- package/plugin/skills/director/SKILL.md +5 -0
- package/plugin/skills/ernie-image/SKILL.md +5 -0
- package/plugin/skills/flux-txt2img/SKILL.md +5 -0
- package/plugin/skills/ideogram-ultra/SKILL.md +5 -0
- package/plugin/skills/installer-packs/SKILL.md +5 -0
- package/plugin/skills/krea2-identity-edit/SKILL.md +5 -0
- package/plugin/skills/krea2-txt2img/SKILL.md +5 -0
- package/plugin/skills/local-llm-free/SKILL.md +5 -0
- package/plugin/skills/lora-manager/SKILL.md +64 -0
- package/plugin/skills/ltx-director/SKILL.md +5 -0
- package/plugin/skills/ltxv2-video/SKILL.md +5 -0
- package/plugin/skills/model-compatibility/SKILL.md +5 -0
- package/plugin/skills/model-registry/SKILL.md +5 -0
- package/plugin/skills/panel-node-pack-sync/SKILL.md +5 -0
- package/plugin/skills/prompt-engineering/SKILL.md +5 -0
- package/plugin/skills/qwen-image-edit/SKILL.md +5 -0
- package/plugin/skills/qwen-txt2img/SKILL.md +5 -0
- package/plugin/skills/report-bug/SKILL.md +5 -0
- package/plugin/skills/rgthree/SKILL.md +5 -0
- package/plugin/skills/train-character-lora/SKILL.md +5 -0
- package/plugin/skills/triton-sageattention/SKILL.md +2 -21
- package/plugin/skills/troubleshooting/SKILL.md +5 -0
- package/plugin/skills/video-extend/SKILL.md +5 -0
- package/plugin/skills/video-upscale/SKILL.md +5 -0
- package/plugin/skills/wan-flf-video/SKILL.md +5 -0
- package/plugin/skills/wan-multitalk/SKILL.md +5 -0
- package/plugin/skills/wan-scail-replacement/SKILL.md +5 -0
- package/plugin/skills/wan-t2v-video/SKILL.md +5 -0
- package/plugin/skills/workflow-layout/SKILL.md +5 -0
- package/plugin/skills/z-image-txt2img/SKILL.md +5 -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
|
|
@@ -72,7 +74,7 @@ function journalTabFor(ctx) {
|
|
|
72
74
|
function journalConversationFor(ctx) {
|
|
73
75
|
return conversationOfScopeAddress(ctx.tabId);
|
|
74
76
|
}
|
|
75
|
-
import { BRIDGE_DEFAULT_TIMEOUT_MS, BRIDGE_READ_DEFAULT_TIMEOUT_MS, dispatchOutcomeOf, isCapabilityRefusal, isPanelCmdUnsupportedError, isReplyTimeoutTagged, isRoutingAmbiguity, } from "../services/ui-bridge.js";
|
|
77
|
+
import { BRIDGE_DEFAULT_TIMEOUT_MS, BRIDGE_READ_DEFAULT_TIMEOUT_MS, dispatchOutcomeOf, isCapabilityRefusal, isPanelCmdUnsupportedError, isReplyTimeoutTagged, isRoutingAmbiguity, requiresWorkflowStampEnforcement, } from "../services/ui-bridge.js";
|
|
76
78
|
import { withWorkflowTarget, } from "../services/workflow-target-store.js";
|
|
77
79
|
import { addUserMcpServer, readUserMcpServers, removeUserMcpServer, setUserMcpServerSecret, } from "../services/user-mcp-config.js";
|
|
78
80
|
import { setComfyuiSecret, setAgentSecret, isAllowedAgentSecretKey, receiptDisclosures, shadowedNote, storeDamageNote, } from "../services/panel-secrets.js";
|
|
@@ -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
|
|
@@ -782,6 +791,26 @@ function isFencedGraphRead(cmd) {
|
|
|
782
791
|
const name = typeof cmd.cmd === "string" ? cmd.cmd : "";
|
|
783
792
|
return name.startsWith("graph_") && !MUTATING_GRAPH_EDIT_CMDS.has(name);
|
|
784
793
|
}
|
|
794
|
+
/**
|
|
795
|
+
* #1778 — a fenced command that is neither a graph edit nor a graph read.
|
|
796
|
+
*
|
|
797
|
+
* `workflow_save` / `workflow_save_as` / `workflow_rename` / `workflow_close`
|
|
798
|
+
* require a workflow stamp (`requiresWorkflowStampEnforcement`) and the
|
|
799
|
+
* dispatch gate refuses them the same way it refuses a graph_* command. They
|
|
800
|
+
* are not in MUTATING_GRAPH_EDIT_CMDS and they do not start with `graph_`, so
|
|
801
|
+
* neither existing probe branch fires. The save handler then returns at
|
|
802
|
+
* `if (res.isError) return res` with the generic "tab may be disconnected"
|
|
803
|
+
* wrapper and an empty corroboration, so a save-only retry never self-heals.
|
|
804
|
+
*
|
|
805
|
+
* Derived from the stamp-enforcement classifier minus the `graph_` prefix, so
|
|
806
|
+
* a newly added active-workflow mutator cannot drift out. The prefix is still
|
|
807
|
+
* load-bearing for the same reason as `isFencedGraphRead`: the diagnosis runs
|
|
808
|
+
* `workflow_list`, which must not re-enter this branch.
|
|
809
|
+
*/
|
|
810
|
+
function isFencedActiveWorkflowMutator(cmd) {
|
|
811
|
+
const name = typeof cmd.cmd === "string" ? cmd.cmd : "";
|
|
812
|
+
return !name.startsWith("graph_") && requiresWorkflowStampEnforcement(cmd);
|
|
813
|
+
}
|
|
785
814
|
/** True when an error is a TRANSIENT transport/reconnect drop (the tab went away
|
|
786
815
|
* or was replaced), NOT a genuine command error or a live-but-frozen reply
|
|
787
816
|
* timeout. Deliberately EXCLUDES "did not reply within N ms" (a backgrounded/
|
|
@@ -844,6 +873,17 @@ function isWorkflowInstanceMismatch(err) {
|
|
|
844
873
|
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
845
874
|
return /workflow instance mismatch/i.test(msg);
|
|
846
875
|
}
|
|
876
|
+
/** Would the next graph command still resolve `ctx.tabId` to a live tab? */
|
|
877
|
+
function sessionRouteIsLive(ctx) {
|
|
878
|
+
if (typeof ctx.bridge.canReach !== "function")
|
|
879
|
+
return true;
|
|
880
|
+
try {
|
|
881
|
+
return ctx.bridge.canReach(ctx.tabId) === true;
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
847
887
|
/**
|
|
848
888
|
* #1331 — the bridge refused a mutation because the active workflow has no identity to
|
|
849
889
|
* fence against (ui-bridge's `no trusted identity` branch).
|
|
@@ -2342,6 +2382,35 @@ async function settleDroppedEnqueue(ctx, res, dispatch) {
|
|
|
2342
2382
|
`when the Manager will not take it. This tool cannot clone for you — it drives whatever ` +
|
|
2343
2383
|
`ComfyUI the panel is bound to, which need not be this machine.`);
|
|
2344
2384
|
}
|
|
2385
|
+
/**
|
|
2386
|
+
* #1699 — a drained Manager queue is not apply_manifest completion when
|
|
2387
|
+
* custom_nodes were never submitted. Keep the panel payload parseable (do not
|
|
2388
|
+
* append prose after the JSON) and name the leftover entries on the object.
|
|
2389
|
+
*/
|
|
2390
|
+
function annotateQueueStatusWithManifestPartial(res) {
|
|
2391
|
+
const leftover = getManifestPartialLeftover();
|
|
2392
|
+
if (!leftover || leftover.not_started.length === 0)
|
|
2393
|
+
return res;
|
|
2394
|
+
if (res.isError)
|
|
2395
|
+
return res;
|
|
2396
|
+
const parsed = parseToolResultJson(res);
|
|
2397
|
+
if (!parsed)
|
|
2398
|
+
return appendNote(res, formatQueueStatusPartialNote(leftover));
|
|
2399
|
+
return {
|
|
2400
|
+
...res,
|
|
2401
|
+
content: [
|
|
2402
|
+
{
|
|
2403
|
+
type: "text",
|
|
2404
|
+
text: JSON.stringify({
|
|
2405
|
+
...parsed,
|
|
2406
|
+
apply_manifest_partial: leftover,
|
|
2407
|
+
queue_complete_for_apply_manifest: false,
|
|
2408
|
+
}, null, 2),
|
|
2409
|
+
},
|
|
2410
|
+
...res.content.slice(1),
|
|
2411
|
+
],
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2345
2414
|
/** Parse a ctx.call ToolResult's text payload as JSON, or null if not parseable. */
|
|
2346
2415
|
function parseToolResultJson(res) {
|
|
2347
2416
|
if (!res || res.isError)
|
|
@@ -2386,6 +2455,31 @@ function isStaleNodeSchemaRefusal(res) {
|
|
|
2386
2455
|
return false;
|
|
2387
2456
|
return /added or retyped since this page loaded its node schema/i.test(textOfToolResult(res));
|
|
2388
2457
|
}
|
|
2458
|
+
/**
|
|
2459
|
+
* #1708 — the panel's add-node guard polls `app.widgets` for a constructor. ComfyUI
|
|
2460
|
+
* 1.49+ puts `getCustomWidgets()` results in the Pinia widget store, so
|
|
2461
|
+
* AUTOCOMPLETE_TEXT_* (LoRA Manager Vue widgets) never appears there. The 5s wait
|
|
2462
|
+
* always expires and the refusal names a reload that cannot help.
|
|
2463
|
+
*/
|
|
2464
|
+
function isLoraManagerAutocompleteRefusal(res) {
|
|
2465
|
+
if (!res.isError)
|
|
2466
|
+
return false;
|
|
2467
|
+
const text = textOfToolResult(res);
|
|
2468
|
+
return (/had no widget after .+ waiting for node extensions to register/i.test(text) &&
|
|
2469
|
+
/AUTOCOMPLETE_TEXT_/i.test(text));
|
|
2470
|
+
}
|
|
2471
|
+
const LORA_MANAGER_AUTOCOMPLETE_NOTE = `\n\nThis is NOT a missing pack and NOT a widget that is still loading. ` +
|
|
2472
|
+
`ComfyUI-LoRA-Manager registers AUTOCOMPLETE_TEXT_* via getCustomWidgets() into ` +
|
|
2473
|
+
`the frontend widget store; the add-node guard only looks at app.widgets (core types), ` +
|
|
2474
|
+
`so reload / panel_refresh_nodes / retry will keep failing. ` +
|
|
2475
|
+
`Add "LoRA Text Loader (LoraManager)" instead — same MODEL/CLIP/trigger_words outputs, ` +
|
|
2476
|
+
`but its lora_syntax input is a standard STRING socket you can drive with panel_set_widget ` +
|
|
2477
|
+
`or a wired string. Core LoraLoader also works. Do not retry this class_type.`;
|
|
2478
|
+
function withLoraManagerAutocompleteNote(res) {
|
|
2479
|
+
if (!isLoraManagerAutocompleteRefusal(res))
|
|
2480
|
+
return res;
|
|
2481
|
+
return appendToolResultText(res, LORA_MANAGER_AUTOCOMPLETE_NOTE);
|
|
2482
|
+
}
|
|
2389
2483
|
function appendToolResultText(res, extra) {
|
|
2390
2484
|
const idx = res?.content?.findIndex((c) => c.type === "text") ?? -1;
|
|
2391
2485
|
const block = idx >= 0 ? res.content[idx] : undefined;
|
|
@@ -2395,6 +2489,62 @@ function appendToolResultText(res, extra) {
|
|
|
2395
2489
|
content[idx] = { type: "text", text: `${block.text}${extra}` };
|
|
2396
2490
|
return { ...res, content };
|
|
2397
2491
|
}
|
|
2492
|
+
// ---- #1695: bound the panel_set_widget previous/new echo --------------------
|
|
2493
|
+
//
|
|
2494
|
+
// Code-mode clients (Codex `functions.exec`) batch several panel_set_widget
|
|
2495
|
+
// calls into one script, and the OUTER tool result accumulates every nested
|
|
2496
|
+
// reply. Each reply echoes the previous and new widget values verbatim; two
|
|
2497
|
+
// long CLIPTextEncode prompts per call push the aggregate past the client's
|
|
2498
|
+
// tool-result budget ("Output exceeded the available model context and was
|
|
2499
|
+
// truncated") even though every mutation succeeded — and the truncation can
|
|
2500
|
+
// hide exactly the per-call status the caller needs. The echo is confirmation,
|
|
2501
|
+
// not transport (a read-back belongs to panel_query_graph), so a long string
|
|
2502
|
+
// value is summarized server-side as { chars, sha256, preview }: the hash keeps
|
|
2503
|
+
// the confirmation verifiable, the preview keeps it legible, and `echo: "full"`
|
|
2504
|
+
// opts back into the verbatim reply.
|
|
2505
|
+
//
|
|
2506
|
+
// Server-side on purpose — the echo is produced by the panel frontend, and an
|
|
2507
|
+
// old panel keeps echoing verbatim no matter what a new one would do (the same
|
|
2508
|
+
// reason fitQueryGraphReply re-fits here, #807).
|
|
2509
|
+
const SET_WIDGET_ECHO_SUMMARY_THRESHOLD = 1000;
|
|
2510
|
+
const SET_WIDGET_ECHO_PREVIEW_CHARS = 160;
|
|
2511
|
+
function summarizeSetWidgetEcho(res, full) {
|
|
2512
|
+
if (full || res.isError)
|
|
2513
|
+
return res;
|
|
2514
|
+
const payload = parseToolResultJson(res);
|
|
2515
|
+
const set = payload?.set;
|
|
2516
|
+
if (!payload || !set || typeof set !== "object" || Array.isArray(set))
|
|
2517
|
+
return res;
|
|
2518
|
+
const record = set;
|
|
2519
|
+
const summarize = (v) => {
|
|
2520
|
+
if (typeof v !== "string" || v.length <= SET_WIDGET_ECHO_SUMMARY_THRESHOLD)
|
|
2521
|
+
return v;
|
|
2522
|
+
return {
|
|
2523
|
+
chars: v.length,
|
|
2524
|
+
sha256: createHash("sha256").update(v, "utf8").digest("hex"),
|
|
2525
|
+
preview: `${v.slice(0, SET_WIDGET_ECHO_PREVIEW_CHARS)}…`,
|
|
2526
|
+
};
|
|
2527
|
+
};
|
|
2528
|
+
const previous = summarize(record.previous);
|
|
2529
|
+
const value = summarize(record.value);
|
|
2530
|
+
// Nothing crossed the threshold — return the reply untouched rather than
|
|
2531
|
+
// dressing up an unfitted payload as a fitted one.
|
|
2532
|
+
if (previous === record.previous && value === record.value)
|
|
2533
|
+
return res;
|
|
2534
|
+
const idx = res.content.findIndex((c) => c.type === "text");
|
|
2535
|
+
if (idx < 0)
|
|
2536
|
+
return res;
|
|
2537
|
+
const fitted = {
|
|
2538
|
+
...payload,
|
|
2539
|
+
set: { ...record, previous, value },
|
|
2540
|
+
echo: "summary",
|
|
2541
|
+
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.',
|
|
2542
|
+
};
|
|
2543
|
+
return {
|
|
2544
|
+
...res,
|
|
2545
|
+
content: res.content.map((c, i) => i === idx && c.type === "text" ? { ...c, text: JSON.stringify(fitted, null, 2) } : c),
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2398
2548
|
// ---- #809: turn the panel's silent `truncated: true` booleans into a remedy --------
|
|
2399
2549
|
//
|
|
2400
2550
|
// A bare boolean is the WORST truncation signal there is: it is a field, not prose, so
|
|
@@ -3586,6 +3736,79 @@ function identityClaimedContentUnverifiedNote(detail) {
|
|
|
3586
3736
|
`expecting the opened file. Retry the graph read (panel_graph_outline) once the tab answers.` +
|
|
3587
3737
|
busy);
|
|
3588
3738
|
}
|
|
3739
|
+
/**
|
|
3740
|
+
* #1710 — restamp extra.comfyui_mcp onto the dest the tab is already bound to,
|
|
3741
|
+
* and copy dest-file promoted widgets onto any live empty slots.
|
|
3742
|
+
*
|
|
3743
|
+
* Fail-closed: dest file unreadable, node-set mismatch, or a non-empty live
|
|
3744
|
+
* widget that disagrees with dest (#1639 previous graph) leaves extra alone.
|
|
3745
|
+
*/
|
|
3746
|
+
async function tryRebindConfirmedOpenIdentity(ctx, destPath) {
|
|
3747
|
+
let live = null;
|
|
3748
|
+
try {
|
|
3749
|
+
const serialized = await ctx.call({ cmd: "graph_serialize" }, 8000);
|
|
3750
|
+
live = workflowFromSerializeReply(parseToolResultJson(serialized));
|
|
3751
|
+
}
|
|
3752
|
+
catch {
|
|
3753
|
+
return { status: "skipped" };
|
|
3754
|
+
}
|
|
3755
|
+
if (!live)
|
|
3756
|
+
return { status: "skipped" };
|
|
3757
|
+
let dest;
|
|
3758
|
+
try {
|
|
3759
|
+
dest = await readWorkflowFromPath(destPath);
|
|
3760
|
+
}
|
|
3761
|
+
catch {
|
|
3762
|
+
return { status: "skipped" };
|
|
3763
|
+
}
|
|
3764
|
+
if (!shouldRebindOpenIdentity({ live, dest, destPath }))
|
|
3765
|
+
return { status: "skipped" };
|
|
3766
|
+
const destUuid = await activeWorkflowUuidForPath(ctx, destPath);
|
|
3767
|
+
const { graph, restoredWidgets } = patchOpenIdentity(live, dest, destPath, destUuid);
|
|
3768
|
+
const loaded = await ctx.call({ cmd: "graph_load", graph }, 30000);
|
|
3769
|
+
if (loaded.isError)
|
|
3770
|
+
return { status: "skipped" };
|
|
3771
|
+
if (destUuid)
|
|
3772
|
+
refreshWorkflowUuid(ctx, { workflow_uuid: destUuid });
|
|
3773
|
+
return { status: "rebound", destPath, destUuid, restoredWidgets };
|
|
3774
|
+
}
|
|
3775
|
+
async function activeWorkflowUuidForPath(ctx, destPath) {
|
|
3776
|
+
try {
|
|
3777
|
+
const list = parseToolResultJson(await ctx.call({ cmd: "workflow_list" }, 6000));
|
|
3778
|
+
if (!list)
|
|
3779
|
+
return undefined;
|
|
3780
|
+
if (readOpenActiveAgainstTarget(list.active, destPath, list.active_confirmed) !== "same") {
|
|
3781
|
+
return undefined;
|
|
3782
|
+
}
|
|
3783
|
+
return responseWorkflowUuid(list.active);
|
|
3784
|
+
}
|
|
3785
|
+
catch {
|
|
3786
|
+
return undefined;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
async function activeSavedPath(ctx) {
|
|
3790
|
+
try {
|
|
3791
|
+
const list = parseToolResultJson(await ctx.call({ cmd: "workflow_list" }, 6000));
|
|
3792
|
+
const path = isPlainObject(list?.active) ? list.active.path : undefined;
|
|
3793
|
+
return typeof path === "string" && path ? path : undefined;
|
|
3794
|
+
}
|
|
3795
|
+
catch {
|
|
3796
|
+
return undefined;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
function openedIdentityReboundResult(destPath, rebound) {
|
|
3800
|
+
return ok({
|
|
3801
|
+
opened: { path: destPath },
|
|
3802
|
+
identity_rebound: true,
|
|
3803
|
+
restored_promoted_widgets: rebound.restoredWidgets,
|
|
3804
|
+
note: `Opened ${destPath}. Frontend-normalized presentation/widget fields differed after load; ` +
|
|
3805
|
+
`extra.comfyui_mcp.workflow_path/workflow_uuid were rebound to this workflow so a later ` +
|
|
3806
|
+
`save will not refuse a source-stamp mismatch.` +
|
|
3807
|
+
(rebound.restoredWidgets > 0
|
|
3808
|
+
? ` Restored ${rebound.restoredWidgets} empty promoted widget value(s) from the saved graph.`
|
|
3809
|
+
: ""),
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3589
3812
|
function identityClaimedButLiveGraphUnchangedNote(ctx) {
|
|
3590
3813
|
const fence = currentWorkflowFence(ctx);
|
|
3591
3814
|
const fenceTxt = fence.known && fence.uuid
|
|
@@ -3600,7 +3823,7 @@ function identityClaimedButLiveGraphUnchangedNote(ctx) {
|
|
|
3600
3823
|
`Read the graph (panel_graph_outline) to see what is actually open, then retry ` +
|
|
3601
3824
|
`panel_open_workflow or panel_load_workflow if the canvas did not switch.`);
|
|
3602
3825
|
}
|
|
3603
|
-
async function clearFenceOnIdentityProvenOpen(ctx, res) {
|
|
3826
|
+
async function clearFenceOnIdentityProvenOpen(ctx, res, requestedPath) {
|
|
3604
3827
|
const text = toolResultText(res);
|
|
3605
3828
|
// ONLY the class that states identity was proven. The UNPROVEN verdict ("could not
|
|
3606
3829
|
// prove that the active canvas was rebound") must keep failing closed: re-deriving a
|
|
@@ -3609,6 +3832,15 @@ async function clearFenceOnIdentityProvenOpen(ctx, res) {
|
|
|
3609
3832
|
return { res, repaired: false };
|
|
3610
3833
|
const canvas = await probeLiveGraphUnderCurrentFence(ctx);
|
|
3611
3834
|
if (canvas.status === "answered") {
|
|
3835
|
+
// #1710 — the old stamp still answering is ALSO what a save-as copy looks
|
|
3836
|
+
// like: dest tab, dest nodes, extra.comfyui_mcp still naming the source.
|
|
3837
|
+
// Confirm dest file content before touching extra (#1639 stays fail-closed).
|
|
3838
|
+
if (requestedPath) {
|
|
3839
|
+
const rebound = await tryRebindConfirmedOpenIdentity(ctx, requestedPath);
|
|
3840
|
+
if (rebound.status === "rebound") {
|
|
3841
|
+
return { res: openedIdentityReboundResult(requestedPath, rebound), repaired: true };
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3612
3844
|
return { res: appendToolResultText(res, identityClaimedButLiveGraphUnchangedNote(ctx)), repaired: false };
|
|
3613
3845
|
}
|
|
3614
3846
|
if (canvas.status === "unanswered") {
|
|
@@ -3793,6 +4025,20 @@ function responseWorkflowUuid(value) {
|
|
|
3793
4025
|
return WORKFLOW_UUID_RE.test(raw) ? raw : undefined;
|
|
3794
4026
|
}
|
|
3795
4027
|
/** Refresh only the bridge-owned command stamp, never caller data. */
|
|
4028
|
+
/** #1656 — tell the orchestrator that a live read confirmed the routed tab's CURRENT
|
|
4029
|
+
* stamp, so a value inherited on a same-socket re-hello stops being treated as
|
|
4030
|
+
* inherited. Never throws, and can never move a fence (see the bridge method). */
|
|
4031
|
+
function corroborateTabStamp(ctx, workflowUuid) {
|
|
4032
|
+
const fn = ctx.bridge.corroborateTabStamp;
|
|
4033
|
+
if (typeof fn !== "function")
|
|
4034
|
+
return false;
|
|
4035
|
+
try {
|
|
4036
|
+
return fn.call(ctx.bridge, ctx.tabId, workflowUuid) === true;
|
|
4037
|
+
}
|
|
4038
|
+
catch {
|
|
4039
|
+
return false;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
3796
4042
|
function refreshWorkflowUuid(ctx, value) {
|
|
3797
4043
|
const uuid = responseWorkflowUuid(value);
|
|
3798
4044
|
const refresh = ctx.bridge.refreshWorkflowUuid;
|
|
@@ -4295,8 +4541,29 @@ async function rebindWorkflowFence(ctx, opts) {
|
|
|
4295
4541
|
// "already current" requires a KNOWN prior fence equal to the live uuid. An
|
|
4296
4542
|
// UNKNOWN prior fence must not short-circuit the adoption: we would be claiming
|
|
4297
4543
|
// the stamp already matched without ever having read it.
|
|
4298
|
-
if (before.known && before.uuid === uuid)
|
|
4544
|
+
if (before.known && before.uuid === uuid) {
|
|
4545
|
+
// #1656 — THE FENCE DOES NOT MOVE HERE, BUT THE EVIDENCE DOES.
|
|
4546
|
+
//
|
|
4547
|
+
// Reaching this line means the panel's own corroborated workflow_list says the
|
|
4548
|
+
// LIVE canvas is this very instance. If the routed tab's stamp was one CARRIED
|
|
4549
|
+
// onto a new tab id by a same-socket re-hello that could not resolve an identity,
|
|
4550
|
+
// that carried value has now been confirmed by an observation of the tab under its
|
|
4551
|
+
// CURRENT id — so the dispatch-time agreement gate must stop treating it as
|
|
4552
|
+
// inherited. Without this, a rename/save whose hello merely RACED the canvas
|
|
4553
|
+
// identity would leave the session refusing edits to a canvas the panel agrees is
|
|
4554
|
+
// the right one: the fix would be worse than the bug.
|
|
4555
|
+
//
|
|
4556
|
+
// This is NOT the retarget #1646 removed, and deliberately not `refreshWorkflowUuid`
|
|
4557
|
+
// either — that one WRITES a stamp (and a conversation's issue-time stamp with it).
|
|
4558
|
+
// `corroborateTabStamp` can only promote the provenance of a uuid the orchestrator
|
|
4559
|
+
// has itself confirmed already equals the tab's current stamp; it writes no stamp and
|
|
4560
|
+
// refuses outright when the values differ, so no target changes and no later edit is
|
|
4561
|
+
// re-aimed. The uuid still goes through the orchestrator's identity validator, never
|
|
4562
|
+
// off parsed prose, and the result is ignored: this is corroboration, and a diagnostic
|
|
4563
|
+
// must never replace the outcome it is describing.
|
|
4564
|
+
corroborateTabStamp(ctx, uuid);
|
|
4299
4565
|
return { status: "already_current", uuid, before };
|
|
4566
|
+
}
|
|
4300
4567
|
// #1646 — a READ-ONLY probe never moves the fence: the live canvas naming a
|
|
4301
4568
|
// DIFFERENT workflow is reported, not adopted. Only a deliberate rebind
|
|
4302
4569
|
// (panel_set_workflow_target, open/new) may replace the fence — a mismatch
|
|
@@ -5051,7 +5318,7 @@ async function openWorkflowWithVerify(path, ctx) {
|
|
|
5051
5318
|
return res;
|
|
5052
5319
|
const probe = await observeActiveAfterOpen(ctx, path);
|
|
5053
5320
|
if (probe.status !== "different") {
|
|
5054
|
-
const cleared = await clearFenceOnIdentityProvenOpen(ctx, res);
|
|
5321
|
+
const cleared = await clearFenceOnIdentityProvenOpen(ctx, res, path);
|
|
5055
5322
|
// #1560 — the probe's SILENCE, reported. Suppressed when the fence was in fact
|
|
5056
5323
|
// repaired: that re-derivation is itself a `workflow_list` round trip, so a
|
|
5057
5324
|
// success there proves the channel answered and the note would contradict the
|
|
@@ -6769,7 +7036,47 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
6769
7036
|
//
|
|
6770
7037
|
// Safe to recommend a retry because a fence refusal is checked BEFORE the handler
|
|
6771
7038
|
// runs — "Nothing was applied" is structural here, not an echoed claim.
|
|
6772
|
-
|
|
7039
|
+
//
|
|
7040
|
+
// #1778 — the same refusal on workflow_save / save_as / rename / close. Those
|
|
7041
|
+
// carry a workflow target but are not graph_*, so neither isMutatingGraphCmd
|
|
7042
|
+
// nor isFencedGraphRead was true and the call fell through to the generic
|
|
7043
|
+
// dispatched:false wrapper: no probe, no corroborateTabStamp, and a
|
|
7044
|
+
// "tab may be disconnected" line that is false here (the tab is connected;
|
|
7045
|
+
// what is stale is the stamp). Widening this branch is the self-heal.
|
|
7046
|
+
//
|
|
7047
|
+
// THE PHRASE IS NOT THE STATE, AND TWO OTHER REFUSALS QUOTE IT.
|
|
7048
|
+
//
|
|
7049
|
+
// `isWorkflowInstanceMismatch` is an unanchored `/workflow instance mismatch/i`,
|
|
7050
|
+
// and the fence is not the only thing that says those words. The bridge's
|
|
7051
|
+
// `"<cmd>" cannot be safely targeted to the active workflow` reject quotes them
|
|
7052
|
+
// inside its `readsNote` — to describe what a DIFFERENT command would get — and
|
|
7053
|
+
// carries the second look-alike, `no trusted identity`, in its `why` clause.
|
|
7054
|
+
// Both are minted pre-dispatch and flagged `dispatched:false`, so both satisfy
|
|
7055
|
+
// the phrase match on their way through here.
|
|
7056
|
+
//
|
|
7057
|
+
// For a mutating graph edit that never surfaced, by ORDERING rather than by
|
|
7058
|
+
// design: the #1401 branch catches `no trusted identity` for `isMutatingGraphCmd`
|
|
7059
|
+
// above. The four active-workflow mutators have no branch above them, so the
|
|
7060
|
+
// widening alone handed each of these two states the OTHER's remedy — measured,
|
|
7061
|
+
// not reasoned. A capability refusal ending "Update the panel and hard-refresh
|
|
7062
|
+
// the browser tab" came back with "Retry once — … panel_open_workflow is the way
|
|
7063
|
+
// to settle which one you mean" appended to it: the futile retry #709 exists to
|
|
7064
|
+
// suppress, contradicting the sentence directly above it, plus a wasted
|
|
7065
|
+
// workflow_list round trip. And a fence-less session got #1330's "the fence
|
|
7066
|
+
// already names the live canvas" for the state whose own docstring says "a
|
|
7067
|
+
// mismatch may clear by itself, this never does".
|
|
7068
|
+
//
|
|
7069
|
+
// So the arm carries the discriminators the graph arm gets from ordering. Both
|
|
7070
|
+
// are TYPED-or-explicit, never a second reading of the same prose:
|
|
7071
|
+
// `isCapabilityRefusal` is the bridge's own symbol marker, and
|
|
7072
|
+
// `isNoTrustedIdentityRefusal` matches a distinct phrase a mismatch refusal never
|
|
7073
|
+
// contains. Scoped to THIS arm on purpose — `isMutatingGraphCmd` keeps its exact
|
|
7074
|
+
// prior behaviour, so nothing about the graph path moves here.
|
|
7075
|
+
if (isWorkflowInstanceMismatch(err) &&
|
|
7076
|
+
(isMutatingGraphCmd(cmd) ||
|
|
7077
|
+
(isFencedActiveWorkflowMutator(cmd) &&
|
|
7078
|
+
!isCapabilityRefusal(err) &&
|
|
7079
|
+
!isNoTrustedIdentityRefusal(err)))) {
|
|
6773
7080
|
const name = typeof cmd.cmd === "string" ? cmd.cmd : "panel command";
|
|
6774
7081
|
const raw = err instanceof Error ? err.message : String(err);
|
|
6775
7082
|
// The uuid the command CARRIED, read from the panel's own refusal rather than
|
|
@@ -8788,7 +9095,7 @@ export function buildPanelToolDefs() {
|
|
|
8788
9095
|
},
|
|
8789
9096
|
},
|
|
8790
9097
|
])),
|
|
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.", {
|
|
9098
|
+
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
9099
|
class_type: z.string().describe("Exact ComfyUI node class_type to create."),
|
|
8793
9100
|
pos: xy()
|
|
8794
9101
|
.optional()
|
|
@@ -8802,7 +9109,7 @@ export function buildPanelToolDefs() {
|
|
|
8802
9109
|
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
9110
|
const first = await add();
|
|
8804
9111
|
if (!isStaleNodeSchemaRefusal(first))
|
|
8805
|
-
return first;
|
|
9112
|
+
return withLoraManagerAutocompleteNote(first);
|
|
8806
9113
|
// #1329 — DO THE REFRESH THE REFUSAL ASKS FOR, instead of billing the caller.
|
|
8807
9114
|
//
|
|
8808
9115
|
// The reporter uploaded two images, and both LoadImage adds were refused with
|
|
@@ -8820,19 +9127,19 @@ export function buildPanelToolDefs() {
|
|
|
8820
9127
|
// The refusal is the better error: it names what is wrong with the SCHEMA and
|
|
8821
9128
|
// what clears it. Carry the refresh failure alongside so the caller knows the
|
|
8822
9129
|
// 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, ` +
|
|
9130
|
+
return withLoraManagerAutocompleteNote(appendToolResultText(first, `\n\n(Tried to clear this automatically: panel_refresh_nodes was dispatched and FAILED, ` +
|
|
8824
9131
|
`so the schema is unchanged and retrying the add will refuse again. ` +
|
|
8825
|
-
`${textOfToolResult(refreshed)})`);
|
|
9132
|
+
`${textOfToolResult(refreshed)})`));
|
|
8826
9133
|
}
|
|
8827
9134
|
const second = await add();
|
|
8828
9135
|
if (!isStaleNodeSchemaRefusal(second))
|
|
8829
|
-
return second;
|
|
9136
|
+
return withLoraManagerAutocompleteNote(second);
|
|
8830
9137
|
// Still stale after a successful refresh: report THAT, because it means the
|
|
8831
9138
|
// remedy the refusal prescribes does not fix this instance and a caller
|
|
8832
9139
|
// following it by hand would loop.
|
|
8833
|
-
return appendToolResultText(second, `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
|
|
9140
|
+
return withLoraManagerAutocompleteNote(appendToolResultText(second, `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
|
|
8834
9141
|
`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.)`);
|
|
9142
|
+
`Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`));
|
|
8836
9143
|
}),
|
|
8837
9144
|
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
9145
|
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 +9534,7 @@ export function buildPanelToolDefs() {
|
|
|
9227
9534
|
node_id: nodeId().describe("Node id whose input to disconnect."),
|
|
9228
9535
|
input: slotRef.optional().describe("Input slot name or index (default 0)."),
|
|
9229
9536
|
}, 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).", {
|
|
9537
|
+
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
9538
|
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
9232
9539
|
widget: z.string().describe("Widget name (e.g. 'steps', 'cfg', 'text')."),
|
|
9233
9540
|
value: z
|
|
@@ -9238,6 +9545,10 @@ export function buildPanelToolDefs() {
|
|
|
9238
9545
|
.boolean()
|
|
9239
9546
|
.optional()
|
|
9240
9547
|
.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`."),
|
|
9548
|
+
echo: z
|
|
9549
|
+
.enum(["summary", "full"])
|
|
9550
|
+
.optional()
|
|
9551
|
+
.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
9552
|
}, async (args, ctx) => {
|
|
9242
9553
|
// Distinguish "value present but empty" from "value absent" by key
|
|
9243
9554
|
// presence, NOT a truthiness check — an empty string is a legitimate
|
|
@@ -9251,7 +9562,14 @@ export function buildPanelToolDefs() {
|
|
|
9251
9562
|
// a single revalidation, #338/#458) — that authoritative fetch can outlast
|
|
9252
9563
|
// the 6000 ms default ack on a large install and return a FALSE timeout.
|
|
9253
9564
|
// Give the guarded write the bounded refresh ack budget.
|
|
9254
|
-
|
|
9565
|
+
// #1695: summarize the previous/new echo of every successful write
|
|
9566
|
+
// (direct, remapped, or inner-subgraph) before anything else touches
|
|
9567
|
+
// it — long prompts batched in one code-mode script otherwise overflow
|
|
9568
|
+
// the outer tool result even though the mutations succeeded. Applied
|
|
9569
|
+
// to the raw success reply so a later appendToolResultText disclosure
|
|
9570
|
+
// (which makes the text no longer parse as JSON) cannot dodge it.
|
|
9571
|
+
const echoFull = args.echo === "full";
|
|
9572
|
+
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
9573
|
const first = await write(args.node_id, args.widget);
|
|
9256
9574
|
if (!first.isError)
|
|
9257
9575
|
return first;
|
|
@@ -9871,6 +10189,13 @@ export function buildPanelToolDefs() {
|
|
|
9871
10189
|
}
|
|
9872
10190
|
}
|
|
9873
10191
|
const scope = args.scope ?? "orchestrator";
|
|
10192
|
+
// #1700 — the frontend soft_reload only drops the websocket (this pack's
|
|
10193
|
+
// /reload is a 503). The live agent keeps the MCP set it was spawned
|
|
10194
|
+
// with, and a plain resume restores the session-recorded set anyway.
|
|
10195
|
+
// Schedule a forked in-process respawn so makeMcpServers() re-reads
|
|
10196
|
+
// ~/.claude.json and the replacement session is NOT the old MCP set.
|
|
10197
|
+
if (scope === "orchestrator")
|
|
10198
|
+
applyMcpReload?.(ctx.tabId);
|
|
9874
10199
|
const res = await ctx.call({ cmd: "soft_reload", scope }, 15000);
|
|
9875
10200
|
// #765 — the agent reads this result as its last context before the
|
|
9876
10201
|
// reload, and again after the resume, so it is the place to state what
|
|
@@ -10599,9 +10924,23 @@ export function buildPanelToolDefs() {
|
|
|
10599
10924
|
if (ctx.awaitReachable && !(await ctx.awaitReachable())) {
|
|
10600
10925
|
return noReachableTabFail(args.name ? "workflow_save_as" : "workflow_save", ctx);
|
|
10601
10926
|
}
|
|
10602
|
-
const
|
|
10603
|
-
?
|
|
10604
|
-
:
|
|
10927
|
+
const saveOnce = () => args.name
|
|
10928
|
+
? ctx.call({ cmd: "workflow_save_as", name: args.name }, 15000)
|
|
10929
|
+
: ctx.call({ cmd: "workflow_save" }, 15000);
|
|
10930
|
+
let res = await saveOnce();
|
|
10931
|
+
// #1710 — dest tab + dest nodes, extra still stamped as the save-as SOURCE.
|
|
10932
|
+
// Rebind extra to the confirmed dest, then retry the same save once.
|
|
10933
|
+
if (res.isError && isStampMismatchSaveRefusal(toolResultText(res))) {
|
|
10934
|
+
const destPath = await activeSavedPath(ctx);
|
|
10935
|
+
if (destPath) {
|
|
10936
|
+
const rebound = await tryRebindConfirmedOpenIdentity(ctx, destPath);
|
|
10937
|
+
if (rebound.status === "rebound") {
|
|
10938
|
+
const retried = await saveOnce();
|
|
10939
|
+
if (!retried.isError)
|
|
10940
|
+
res = retried;
|
|
10941
|
+
}
|
|
10942
|
+
}
|
|
10943
|
+
}
|
|
10605
10944
|
if (res.isError)
|
|
10606
10945
|
return res;
|
|
10607
10946
|
// #1045 — a SAVE-AS replaces the active workflow instance: the canvas is
|
|
@@ -11040,19 +11379,50 @@ export function buildPanelToolDefs() {
|
|
|
11040
11379
|
// known-negative write case is stated rather than papered over.
|
|
11041
11380
|
let probeRefused;
|
|
11042
11381
|
let probeOk = false;
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11382
|
+
const probeOnce = async () => {
|
|
11383
|
+
probeRefused = undefined;
|
|
11384
|
+
probeOk = false;
|
|
11385
|
+
try {
|
|
11386
|
+
// The cheapest fenced read there is: ids only, one row. It is refused by the
|
|
11387
|
+
// same instance fence every graph command carries, which is exactly the
|
|
11388
|
+
// question — a full outline would answer it no better and would cost the caller
|
|
11389
|
+
// a page of graph on a recovery path.
|
|
11390
|
+
const probe = await ctx.call({ cmd: "graph_query", fields: "ids", limit: 1 }, 8000);
|
|
11391
|
+
if (!probe.isError)
|
|
11392
|
+
probeOk = true;
|
|
11393
|
+
else
|
|
11394
|
+
probeRefused = isWorkflowInstanceMismatch(toolResultText(probe));
|
|
11395
|
+
}
|
|
11396
|
+
catch (err) {
|
|
11397
|
+
probeRefused = isWorkflowInstanceMismatch(err);
|
|
11398
|
+
}
|
|
11399
|
+
};
|
|
11400
|
+
await probeOnce();
|
|
11401
|
+
// #1703 — a passing graph_query is not evidence the NEXT graph command
|
|
11402
|
+
// will route. After a restart the scope pin can still name the OLD
|
|
11403
|
+
// wf:<route>:<path> while the reconnect has already hellod under a new
|
|
11404
|
+
// one; ensureReachable will not move a scope-bound ctx, so claiming
|
|
11405
|
+
// "no recovery needed" stranded the following panel_graph_outline with
|
|
11406
|
+
// "no connected tab". Confirm the live route; if it is dead, take the
|
|
11407
|
+
// same explicit rebind the caller would get from repeating this tool,
|
|
11408
|
+
// and only then re-probe on the tab the next command will use.
|
|
11409
|
+
if (probeOk && !sessionRouteIsLive(ctx)) {
|
|
11410
|
+
try {
|
|
11411
|
+
ctx.rebindToActiveTab?.({ scopeRecoveryConsent: true });
|
|
11412
|
+
}
|
|
11413
|
+
catch {
|
|
11414
|
+
// a failed rebind is not a fence verdict
|
|
11415
|
+
}
|
|
11416
|
+
if (sessionRouteIsLive(ctx))
|
|
11417
|
+
await probeOnce();
|
|
11418
|
+
else {
|
|
11419
|
+
probeOk = false;
|
|
11420
|
+
probeRefused = undefined;
|
|
11421
|
+
}
|
|
11053
11422
|
}
|
|
11054
|
-
|
|
11055
|
-
|
|
11423
|
+
if (probeOk && !sessionRouteIsLive(ctx)) {
|
|
11424
|
+
probeOk = false;
|
|
11425
|
+
probeRefused = undefined;
|
|
11056
11426
|
}
|
|
11057
11427
|
if (probeOk) {
|
|
11058
11428
|
probePassed = true;
|
|
@@ -11476,7 +11846,15 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
11476
11846
|
assertPanelNotTargetedUnverifiable("panel_update_node", args.id);
|
|
11477
11847
|
return ctx.call({ cmd: "graph_update_node", id: args.id, version: args.version, channel: args.channel, mode: args.mode }, 30000);
|
|
11478
11848
|
}),
|
|
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.
|
|
11849
|
+
def("panel_node_queue_status", "Check the built-in Manager's install/update queue status (to see if a queued install finished). Read-only. " +
|
|
11850
|
+
"A drained queue (total_count: 0, is_processing: false) only means THIS queue is idle — " +
|
|
11851
|
+
"it does not include apply_manifest custom_nodes that were never submitted (#1699). " +
|
|
11852
|
+
"When apply_manifest returned a PARTIAL INSTALL, this tool names the leftover entries and " +
|
|
11853
|
+
"sets queue_complete_for_apply_manifest:false; do not restart ComfyUI until those are " +
|
|
11854
|
+
"submitted (re-run apply_manifest).", {}, async (_args, ctx) => {
|
|
11855
|
+
const res = await ctx.call({ cmd: "nodes_queue_status" }, 20000);
|
|
11856
|
+
return annotateQueueStatusWithManifestPartial(res);
|
|
11857
|
+
}),
|
|
11480
11858
|
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
11859
|
// Whole-handler budget (#536): confirm + dispatch + readiness — INCLUDING
|
|
11482
11860
|
// the legacy path's UNPREEMPTIBLE synchronous execSync blocks — must ALL finish
|