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),
100 lines • 4.97 kB
JavaScript
/**
* "SOMETHING ANSWERED" — the fact that separates an ACKNOWLEDGED panel error from a
* channel that never came back (#1560).
*
* ## Why this exists
*
* `ctx.call` flattens every failure into one shape: a ToolResult with `isError: true`
* and some text. That is the right shape for a caller who only has to report the
* failure, and the wrong shape for a caller who has to say WHY it failed — because
* two opposite observations arrive identical:
*
* the panel replied `ok:false` the tab is alive and served the command
* the read never came back the tab did not answer at all
*
* `panel_open_workflow`'s post-open probe (`observeActiveAfterOpen`) is exactly that
* second kind of caller: on a silent read it prints `PANEL CHANNEL: NOT ANSWERING`
* and sends the user to hard-refresh their ComfyUI tab. Told only `isError`, it makes
* that claim about a panel that demonstrably ANSWERED — a false statement in the
* bug's own direction, and a remedy for a problem the user does not have.
*
* ## Why it is not read out of the message
*
* The same reason `panel-refusal.ts` is not: acknowledged panel errors travel as
* arbitrary `msg.error` text, so any sentence one class writes is a sentence the
* other can also contain. Worse for a PROSE predicate specifically — the panel is
* translated, so a rule matching English words is a rule that stops working for
* eleven of twelve locales, silently, in whichever direction the default takes.
*
* So the bridge states it structurally at the ONE place that knows: the branch that
* turns a received `ok:false` reply into a rejection. Nothing else mints it, and the
* absence of the mark is not evidence of anything except that nothing said otherwise.
*
* ## The direction it fails in
*
* A missing mark reads as "did not answer", which is the direction #1560 exists to
* report: the pre-#1560 code swallowed a silent probe entirely, and defaulting the
* other way would restore that silence for every failure class nobody remembered to
* mark. The cost of the mark being wrongly ABSENT is one over-stated note; the cost
* of it being wrongly PRESENT is the reporter's original dead end. That asymmetry is
* why the mark is attached only where a reply was genuinely received, and why it is
* propagated (never re-derived) when an error is substituted downstream.
*/
import { hasOwnField } from "./panel-refusal.js";
/** The property the bridge attaches to an error it minted from a RECEIVED reply.
* Namespaced for the same reason `cmcpPanelRefusal` is: `code`/`cause` on an Error
* already mean other things to other catch blocks. */
export const PANEL_ANSWERED_PROP = "cmcpPanelAnswered";
/**
* Mark an error as "the panel served this command and replied".
*
* `defineProperty`, not assignment — an assignment to a property that exists on the
* prototype as non-writable throws in strict mode, which would replace a clear panel
* error with an unrelated TypeError (the trap `attachPanelRefusal` documents). And
* decorating an error must never be able to fail the command harder than it already
* failed, so the whole thing is best-effort.
*/
export function attachPanelAnswered(err) {
try {
Object.defineProperty(err, PANEL_ANSWERED_PROP, {
value: true,
writable: true,
enumerable: false,
configurable: true,
});
}
catch {
// Best effort: an un-markable error falls back to the honest default (unknown,
// reported as "did not answer"), which is how every older path already behaves.
}
return err;
}
/**
* Carry the mark from an error onto the error that REPLACES it.
*
* The bridge rewrites an old panel's "Unknown command" rejection into an actionable
* update-your-panel error. That rewrite is still a REPLY — the panel answered, it
* just answered that it does not know the command — so dropping the mark there would
* label a talking panel silent, which is the whole defect. Propagated from the
* source, never re-derived from the replacement's text.
*/
export function inheritPanelAnswered(from, to) {
return isPanelAnsweredError(from) ? attachPanelAnswered(to) : to;
}
/**
* Did this error come from a reply the panel actually sent?
*
* OWN property, `true` exactly. An inherited `cmcpPanelAnswered` — from a polluted
* prototype, or a library that decorates Error — would silence a genuine
* NOT-ANSWERING report, which is the reporter's dead end restored. `hasOwnField`
* binds `hasOwnProperty` at module load rather than reading it off the object under
* test, so the check cannot be answered by the thing it distrusts (#1529 P0).
*/
export function isPanelAnsweredError(err) {
if (!err || typeof err !== "object")
return false;
if (!hasOwnField(err, PANEL_ANSWERED_PROP))
return false;
return err[PANEL_ANSWERED_PROP] === true;
}
//# sourceMappingURL=panel-answered.js.map