comfyui-mcp
Version:
Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),
238 lines • 16 kB
JavaScript
// #952 (panel) — an interactive question card was told to go and check the
// render queue.
//
// `panel_ask` was interrupted by a tab disconnect, and the OUTCOME UNKNOWN error
// ended with the only remedy this path has ever offered:
//
// Verify before retrying (e.g. check queue action:"list" /
// get_image (action:"list_outputs")) instead of re-issuing it blindly.
//
// The reporter's objection is exact: "The suggested verification examples concern
// render queues/media and do not apply to an ask card." Neither of those tools
// can tell you whether a question is currently on a human's screen.
//
// WHY THIS IS NOT COSMETIC. The panel-side trace on that issue established that
// for THIS trigger a blind retry really does duplicate the card: the dedupe
// ledger is keyed by the socket's bridge epoch, a reconnect mints a new one, so
// the retry lands in a different scope, `lookupRetry` misses, and it fails open
// and re-executes. Failing open is right for a read or an idempotent write —
// re-running one is cheaper than refusing. It is wrong for a question, because
// the cost is a second card in front of a person and there is no way to withdraw
// the first.
//
// So the remedy has to say what actually applies. What it must NOT do is invent
// a recovery: there is no pending-card query today, and `retry_of` does not help
// across a reconnect for the reason above. Both of those are open design
// questions on the issue and this does not pre-empt them.
/** Panel commands that put something in front of a HUMAN and wait. */
const INTERACTIVE_COMMANDS = new Set(["ask_user", "request_secret"]);
export function isInteractiveCommand(cmd) {
return INTERACTIVE_COMMANDS.has(String(cmd ?? "").trim());
}
/**
* The "verify before retrying" clause, chosen by what the command actually did.
*
* The point of OUTCOME UNKNOWN is that the caller checks instead of retrying blind.
* That only works if the check named can actually hold the answer — and for a graph
* write it did not. Every non-interactive command got the same sentence:
*
* Verify before retrying (e.g. check queue action:"list" /
* get_image (action:"list_outputs"))
*
* which is RENDER evidence. panel#646's interrupted command was `graph_set_widget`.
* Neither the queue nor the output list can say whether a widget edit landed, so an
* agent following that advice looks somewhere structurally incapable of answering,
* learns nothing, and ends up exactly where the message was trying to keep it from:
* retry blindly or give up. The old doc here asserted the queue check was "real
* evidence for a run or a write". It is evidence for a run.
*
* So the kinds are separated by WHAT CAN BE OBSERVED:
*
* • an INTERACTIVE card — nothing in the tool surface can observe it, the answer
* will NEVER arrive, and a retry duplicates the card in front of a human.
* • a RUN — the queue and the output list are exactly right, and stay.
* • a GRAPH WRITE — read the graph back. A graph read is idempotent and safe to
* run the instant a tab is connected, which makes it a better check than
* anything the run path offers. This branch is LAST among the graph_* kinds:
* the prefix alone does not mean the effect is on the canvas, and the two cases
* above are the ones where it is not.
* • a WORKFLOW mutator — the workflow list, not the graph: these change which
* workflows exist or are open, which a canvas read does not see.
* • a LIBRARY write (`graph_save_subgraph`) — panel_list_subgraphs. The graph is
* identical after a blueprint save, so both graph readers show what they showed
* before and prove nothing (codex review).
* • a CLIPBOARD write (`graph_copy_nodes`) — nothing can read the clipboard, and
* the graph is unchanged. Re-issuing is the right answer: a second copy of the
* same nodes is the same clipboard.
* • a MEDIA display (`show_media`) — nothing can read the chat back either, but
* re-issuing is safe in a different way: a duplicate media card costs the user
* nothing, so the retry IS the settlement (panel#1217).
* • anything else — say to verify without naming tools that may not apply.
* Naming a plausible-but-wrong check is what this fix exists to remove; doing
* it again in the default branch would be the same defect with a different tool.
*
* Classified by NAME here rather than by importing GRAPH_CMD_EFFECT, for two
* reasons: ui-bridge.ts imports this module, so reading its table would be a cycle;
* and that table answers a different question (does this need the workflow fence),
* which is not the same as what evidence exists for it afterwards.
*/
/** The one command whose evidence really is the render queue (panel#646). */
const RUN_COMMAND = "graph_run";
/** Writes to the user's blueprint LIBRARY, not to the canvas (codex review, P1).
* `graph_save_subgraph` publishes a subgraph as a reusable blueprint; the graph is
* unchanged afterwards, so both graph readers show exactly what they showed before
* and cannot tell a save that landed from one that did not. `panel_list_subgraphs`
* is the reader that can. */
const LIBRARY_COMMANDS = new Set(["graph_save_subgraph"]);
/** Writes the CLIPBOARD, which nothing in the tool surface can read (codex review).
* Copying leaves the graph identical, so a canvas read is no evidence at all —
* but re-issuing is harmless: a second copy of the
* same nodes produces the same clipboard. That makes "just do it again" the
* correct advice rather than a dangerous one. */
const CLIPBOARD_COMMANDS = new Set(["graph_copy_nodes"]);
/** Media display into the panel's chat (`show_media`, panel#1217). Like the
* interactive branch, nothing in the tool surface can READ BACK whether the
* card painted — the chat has no reader — so the interrupted command is
* unverifiable. Unlike the interactive branch, a blind retry is not dangerous:
* the source file is unchanged, so re-issuing either paints the card that
* never landed or shows the same media twice, and a duplicate media card asks
* nothing of the user — no GPU time (a run), no answer owed (a question).
* That makes "re-issue, and say the repeat may appear" the honest settlement
* rather than the generic "verify first", which names a check that cannot
* exist. */
const MEDIA_COMMANDS = new Set(["show_media"]);
/** The four active-workflow mutators. A canvas read cannot see these — they change
* which workflows exist or are open, not what is on the current graph. Mirrors
* ACTIVE_WORKFLOW_MUTATORS in ui-bridge.ts, which cannot be imported here (it
* imports this module). Duplicated deliberately and kept honest by a test that
* reads the ui-bridge source and compares the two. */
const WORKFLOW_MUTATORS = new Set([
"workflow_save",
"workflow_save_as",
"workflow_rename",
"workflow_close",
]);
/** Workflow NAVIGATION/creation. Deliberately not in ACTIVE_WORKFLOW_MUTATORS — they
* carry their own explicit or new target rather than mutating active content, so the
* fence treats them differently. The question HERE is evidence, not fencing — and the
* evidence for these is NOT the workflow list itself, which is where the first attempt
* went wrong.
*
* Found by running the LIVE panel's registered command names through this classifier
* after the fix merged — these two fell to the default branch, which names no tool.
*
* The first attempt sent them to `panel_list_workflows` on the reasoning that an opened
* workflow appears there. Codex caught it, and the panel had already written the
* rebuttal: `active` matching your target is NOT proof your open ran, because after a
* backend reconnect the frontend restores a tab BY ITSELF (#433). Naming that reader
* would have been the exact defect this module exists to remove — a confident check
* that cannot answer — for the one command class that already has a purpose-built
* correct mechanism.
*
* That mechanism is the panel's OPEN RECEIPTS (#402): every workflow_open/workflow_new
* that RAN is journaled with the selector it was asked for, the identity it resolved
* to, and whether it applied. The latest one rides in the workflow-list reply as
* `last_open` — that is the reply KEY; the panel's local variable is `lastOpenReceipt`,
* and naming THAT is how the first version of this shipped a field no reply carries
* (codex round 2), which is the very defect this module exists to remove. It answers
* ONLY for the command whose id equals its `rid` —
* so an EARLIER successful open of the same file is not evidence about a later dropped
* one. The message says both halves, because the receipt is over-readable without the
* rule that comes with it. */
const WORKFLOW_NAVIGATION = new Set(["workflow_open", "workflow_new"]);
export function midCommandVerifyClause(cmd) {
if (isInteractiveCommand(cmd)) {
// WAITING IS NOT A RECOVERY, and an earlier draft of this said it was
// (codex). Checked against the panel: `redactSensitiveReply` replaces an
// `ask_user`/`request_secret` reply that cannot cross the dropped connection
// with a payload-free failure — deliberately, because the content is the
// user's own input and a replay could land on a different orchestrator. Its
// own words are "ask again on the current connection". So the answer to the
// card already on screen will NOT reach you, however long you wait.
const reissue = cmd === "request_secret"
? // NEVER route a secret through the conversation (codex). The whole
// point of this card is a masked input the agent never sees and that
// is never written to chat history; "just ask them for it" would
// defeat the guarantee the tool makes.
`Re-issue panel_request_secret on the current connection — that is what the panel itself ` +
`advises, and it is the ONLY safe route: never ask the user to paste a secret into the ` +
`conversation, where you would see it and it would be recorded.`
: `Re-issue the question on the current connection, or simply ask it in conversation.`;
return (`Nothing in the tool surface can check that — ` +
`there is no pending-card query — and the answer to it will NOT reach you: the panel ` +
`deliberately does not replay a reply of this kind across a reconnect (nothing was applied ` +
`and nothing was stored). ${reissue} Expect the stale card to remain on screen: retry ` +
`suppression is keyed to the socket that dropped, so this counts as a new command and the ` +
`user may see two. Tell them which one to answer.`);
}
if (cmd === RUN_COMMAND) {
// The one command the original sentence was actually right about.
return (`Verify before retrying (check queue action:"list" / get_image ` +
`(action:"list_outputs")) instead of re-issuing it blindly — a second run costs ` +
`GPU time and produces a duplicate output.`);
}
if (WORKFLOW_NAVIGATION.has(cmd)) {
return (`Verify with panel_list_workflows and read its \`last_open\` — the panel's own ` +
`execution record for opens (#402). It answers ONLY for the command whose id equals ` +
`its \`rid\`: \`applied:true\` means it completed, \`applied:false\` means nothing was ` +
`applied and it is safe to re-issue, \`applied:"unknown"\` means it may have taken ` +
`effect and must NOT be blindly retried. Do NOT conclude anything from \`active\` ` +
`matching your target, and do not read an EARLIER receipt for the same file as ` +
`evidence about this one — after a backend reconnect the frontend restores a tab by ` +
`itself, which explains a matching \`active\` without your command ever running. A ` +
`blind retry costs: a second workflow_new leaves the user with two empty tabs.`);
}
if (WORKFLOW_MUTATORS.has(cmd)) {
return (`Verify with panel_list_workflows before retrying — these change which workflows ` +
`exist or are open, which a canvas read does not see. Re-issuing blindly can save ` +
`or close a second time.`);
}
if (LIBRARY_COMMANDS.has(cmd)) {
return (`Verify with panel_list_subgraphs before retrying — this writes to the blueprint ` +
`LIBRARY, not to the canvas, so the graph is identical either way and a graph read ` +
`cannot tell a save that landed from one that did not. If the blueprint is already ` +
`there, do NOT re-issue it: a second save collides on the name.`);
}
if (CLIPBOARD_COMMANDS.has(cmd)) {
return (`Nothing in the tool surface can check that — the clipboard is not readable, and the ` +
`graph is unchanged either way, so neither graph reader is evidence. Just re-issue it: ` +
`unlike the other interrupted writes, copying the same nodes twice produces the same ` +
`clipboard, so a retry here costs nothing and settles it.`);
}
if (MEDIA_COMMANDS.has(cmd)) {
return (`Nothing in the tool surface can check that — the panel's chat cannot be read back, ` +
`so you cannot see whether the card painted. Re-issue the same panel_show_media on the ` +
`current connection: the source file is unchanged, so the retry either paints the card ` +
`that never landed or shows the same media twice — and a duplicate media card asks ` +
`nothing of the user, unlike a second render (GPU time) or a second question card (an ` +
`answer owed). Retry suppression is keyed to the socket that dropped, so the retry runs ` +
`fresh and the user may see two; if it appears twice, tell them the repeat is the retry, ` +
`not new content. If even that is unwanted, ask the user whether it appeared first.`);
}
if (cmd.startsWith("graph_")) {
return (`Verify with a graph READ before retrying — panel_query_graph on the node(s) this ` +
`touched (or panel_graph_outline for a structural change) and compare against what ` +
`you asked for. A read is idempotent, so it is safe the moment a tab is connected, ` +
`and it is the only check that can see a canvas edit: the render queue and the ` +
`output list cannot (panel#646). If the read shows the change already applied, do ` +
`NOT re-issue it. If the command only moved the view or the selection, it changed ` +
`no graph content, so there is nothing to verify and nothing worth re-issuing.`);
}
// Deliberately names no tool. The defect being fixed was a confidently wrong
// check; an unrecognised command is exactly where a guess would be wrong again.
return (`Verify that it applied before retrying, rather than re-issuing it blindly — the ` +
`command reached the panel, so a retry may apply it twice.`);
}
/** The full OUTCOME UNKNOWN sentence for a mid-command disconnect. */
export function midCommandDisconnectMessage(opts) {
const interactive = isInteractiveCommand(opts.cmd);
const media = MEDIA_COMMANDS.has(opts.cmd);
const applied = interactive
? `the command was already sent, so the card may already be on screen`
: media
? `the command was already sent, so the media may already be on screen in the panel's chat`
: `the command was already sent, so the panel may have applied it (for a run, ComfyUI may already be rendering)`;
return (`panel tab ${opts.short} disconnected mid-command ("${opts.cmd}") — OUTCOME UNKNOWN: ` +
`${applied}. ${midCommandVerifyClause(opts.cmd)}`);
}
//# sourceMappingURL=mid-command-remedy.js.map