comfyui-mcp 0.52.7 → 0.52.13

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.
Files changed (53) hide show
  1. package/README.md +2 -1
  2. package/dist/boot.js +1 -1
  3. package/dist/boot.js.map +1 -1
  4. package/dist/comfyui/fetch.js +2 -2
  5. package/dist/config.js +11 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/orchestrator/index.js +46 -2
  8. package/dist/orchestrator/index.js.map +1 -1
  9. package/dist/orchestrator/panel-tools.js +218 -18
  10. package/dist/orchestrator/panel-tools.js.map +1 -1
  11. package/dist/orchestrator/run-completion-journal.js +38 -0
  12. package/dist/orchestrator/run-completion-journal.js.map +1 -1
  13. package/dist/orchestrator/run-completion-watchdog.js +189 -0
  14. package/dist/orchestrator/run-completion-watchdog.js.map +1 -0
  15. package/dist/services/comfy-cli.js +23 -10
  16. package/dist/services/comfy-cli.js.map +1 -1
  17. package/dist/services/env-capabilities.js +69 -11
  18. package/dist/services/env-capabilities.js.map +1 -1
  19. package/dist/services/extra-paths.js +211 -14
  20. package/dist/services/extra-paths.js.map +1 -1
  21. package/dist/services/live-interpreter.js +197 -11
  22. package/dist/services/live-interpreter.js.map +1 -1
  23. package/dist/services/manifest.js +50 -71
  24. package/dist/services/manifest.js.map +1 -1
  25. package/dist/services/node-management.js +30 -24
  26. package/dist/services/node-management.js.map +1 -1
  27. package/dist/services/node-snapshots.js +22 -8
  28. package/dist/services/node-snapshots.js.map +1 -1
  29. package/dist/services/panel-launcher.js +198 -33
  30. package/dist/services/panel-launcher.js.map +1 -1
  31. package/dist/services/panel-secrets.js +8 -0
  32. package/dist/services/panel-secrets.js.map +1 -1
  33. package/dist/services/skill-generator.js +3 -2
  34. package/dist/services/skill-generator.js.map +1 -1
  35. package/dist/services/ui-bridge.js +5 -4
  36. package/dist/services/ui-bridge.js.map +1 -1
  37. package/dist/services/update-comfyui.js +9 -10
  38. package/dist/services/update-comfyui.js.map +1 -1
  39. package/dist/services/workflow-lock.js +24 -21
  40. package/dist/services/workflow-lock.js.map +1 -1
  41. package/dist/services/workspace-env.js +158 -10
  42. package/dist/services/workspace-env.js.map +1 -1
  43. package/dist/tools/comfy-cli.js +1 -1
  44. package/dist/tools/comfy-cli.js.map +1 -1
  45. package/dist/tools/install-comfyui.js +2 -2
  46. package/dist/tools/install-comfyui.js.map +1 -1
  47. package/dist/tools/manifest.js +1 -1
  48. package/dist/tools/manifest.js.map +1 -1
  49. package/dist/tools/skills-access.js +5 -3
  50. package/dist/tools/skills-access.js.map +1 -1
  51. package/dist/tools/workflow-library.js +2 -2
  52. package/dist/tools/workflow-library.js.map +1 -1
  53. package/package.json +1 -1
@@ -74,7 +74,7 @@ function journalTabFor(ctx) {
74
74
  function journalConversationFor(ctx) {
75
75
  return conversationOfScopeAddress(ctx.tabId);
76
76
  }
77
- import { BRIDGE_DEFAULT_TIMEOUT_MS, BRIDGE_READ_DEFAULT_TIMEOUT_MS, dispatchOutcomeOf, isCapabilityRefusal, isPanelCmdUnsupportedError, isReplyTimeoutTagged, isRoutingAmbiguity, requiresWorkflowStampEnforcement, } from "../services/ui-bridge.js";
77
+ import { BRIDGE_DEFAULT_TIMEOUT_MS, BRIDGE_READ_DEFAULT_TIMEOUT_MS, dispatchOutcomeOf, GRAPH_CMD_EFFECT, isCapabilityRefusal, isPanelCmdUnsupportedError, isReplyTimeoutTagged, isRoutingAmbiguity, requiresWorkflowStampEnforcement, } from "../services/ui-bridge.js";
78
78
  import { withWorkflowTarget, } from "../services/workflow-target-store.js";
79
79
  import { addUserMcpServer, readUserMcpServers, removeUserMcpServer, setUserMcpServerSecret, } from "../services/user-mcp-config.js";
80
80
  import { setComfyuiSecret, setAgentSecret, isAllowedAgentSecretKey, receiptDisclosures, shadowedNote, storeDamageNote, } from "../services/panel-secrets.js";
@@ -2545,6 +2545,109 @@ function summarizeSetWidgetEcho(res, full) {
2545
2545
  content: res.content.map((c, i) => i === idx && c.type === "text" ? { ...c, text: JSON.stringify(fitted, null, 2) } : c),
2546
2546
  };
2547
2547
  }
2548
+ // ---- #1658: Anima/Krea2 regional canvas prompt textareas --------------------------
2549
+ //
2550
+ // ComfyUI_LC123_nodes `web/inline_regional_canvas.js` hides the prompt widgets
2551
+ // and keeps the authoritative string in `node.properties.animaPrompts[name]`
2552
+ // plus a custom <textarea>. `promptValue()` prefers the map; APPLY /
2553
+ // `savePrompts()` copies the textarea back over `widget.value`. The widgets
2554
+ // do NOT declare `options.property`, so the panel#1363 bound-property verify
2555
+ // never fires and `applyWidgetWrite`'s `w.value` read-back is a tautology.
2556
+ //
2557
+ // `savePrompts` / `setPromptValue` are closures, not methods on the node —
2558
+ // there is no route (the LTXDirector / PromptRelay shape). This is the
2559
+ // node-keyed refusal (#983 rgthree Fast Groups, #1569 Ideogram4). quality /
2560
+ // scene / negative have a real Python-side escape: execute() prefers the
2561
+ // `*_prompt_in` sockets. red / green / blue have none.
2562
+ const ANIMA_REGIONAL_CANVAS_TYPES = new Set([
2563
+ "AnimaRegionalCanvasInline",
2564
+ "Krea2RegionalCanvasInline",
2565
+ "AnimaRegionalCanvas",
2566
+ "AnimaRegionalInpaintCanvas",
2567
+ ]);
2568
+ const ANIMA_REGIONAL_PROMPT_WIDGETS = new Set([
2569
+ "quality_prompt",
2570
+ "scene_prompt",
2571
+ "red_prompt",
2572
+ "green_prompt",
2573
+ "blue_prompt",
2574
+ "negative_prompt",
2575
+ ]);
2576
+ const ANIMA_REGIONAL_WIRED_PROMPT_INPUT = {
2577
+ quality_prompt: "quality_prompt_in",
2578
+ scene_prompt: "scene_prompt_in",
2579
+ negative_prompt: "negative_prompt_in",
2580
+ };
2581
+ function isAnimaRegionalPromptWidget(widget) {
2582
+ return ANIMA_REGIONAL_PROMPT_WIDGETS.has(widget);
2583
+ }
2584
+ /** Type of the single node a compact/detail `graph_query` named, or null when
2585
+ * the reply does not identify one. Fail-open on an unreadable probe. */
2586
+ function parseQueriedNodeType(payload) {
2587
+ if (!payload)
2588
+ return null;
2589
+ const nodes = payload.nodes;
2590
+ if (Array.isArray(nodes)) {
2591
+ for (const n of nodes) {
2592
+ if (!n || typeof n !== "object" || Array.isArray(n))
2593
+ continue;
2594
+ const rec = n;
2595
+ const t = rec.type ?? rec.class_type;
2596
+ if (typeof t === "string" && t)
2597
+ return t;
2598
+ }
2599
+ }
2600
+ const text = typeof payload.text === "string" ? payload.text : "";
2601
+ for (const line of text.split("\n")) {
2602
+ const trimmed = line.trim();
2603
+ // Compact: `#2768 AnimaRegionalCanvasInline "title" · …`
2604
+ const compact = trimmed.match(/^#\S+\s+(\S+)/);
2605
+ if (compact)
2606
+ return compact[1];
2607
+ // Detail JSON line: `{"id":2768,"type":"AnimaRegionalCanvasInline",…}`
2608
+ if (trimmed.startsWith("{")) {
2609
+ try {
2610
+ const row = JSON.parse(trimmed);
2611
+ const t = row.type ?? row.class_type;
2612
+ if (typeof t === "string" && t)
2613
+ return t;
2614
+ }
2615
+ catch {
2616
+ /* not a detail row */
2617
+ }
2618
+ }
2619
+ }
2620
+ return null;
2621
+ }
2622
+ function animaRegionalPromptRefusal(type, widget) {
2623
+ const wired = ANIMA_REGIONAL_WIRED_PROMPT_INPUT[widget];
2624
+ const route = wired
2625
+ ? `Wire a PrimitiveStringMultiline into this node's \`${wired}\` socket — execute() prefers that input over the widget.`
2626
+ : `There is no input socket for "${widget}"; the custom textarea is the only durable store.`;
2627
+ return fail(`panel_set_widget cannot set "${widget}" on ${type}. ` +
2628
+ `The node's custom JS stores the prompt in node.properties.animaPrompts["${widget}"] and a hidden textarea; ` +
2629
+ `APPLY / savePrompts() copies the textarea back over widget.value, so a success here would be a lie. ` +
2630
+ `${route} Do not retry panel_set_widget on this widget.`);
2631
+ }
2632
+ /** Refuse the known LC123 regional-canvas prompt widgets. Identity is read
2633
+ * from a one-node `graph_query`; anything else (query failed, type unreadable,
2634
+ * some other node that happens to own `negative_prompt`) is fail-open. */
2635
+ async function refuseAnimaRegionalPromptWrite(ctx, nodeId, widget) {
2636
+ if (!isAnimaRegionalPromptWidget(widget))
2637
+ return null;
2638
+ const probe = await ctx.call({
2639
+ cmd: "graph_query",
2640
+ ids: [nodeId],
2641
+ fields: "compact",
2642
+ limit: 1,
2643
+ });
2644
+ if (probe.isError)
2645
+ return null;
2646
+ const type = parseQueriedNodeType(parseToolResultJson(probe));
2647
+ if (!type || !ANIMA_REGIONAL_CANVAS_TYPES.has(type))
2648
+ return null;
2649
+ return animaRegionalPromptRefusal(type, widget);
2650
+ }
2548
2651
  // ---- #809: turn the panel's silent `truncated: true` booleans into a remedy --------
2549
2652
  //
2550
2653
  // A bare boolean is the WORST truncation signal there is: it is a field, not prose, so
@@ -7179,21 +7282,22 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
7179
7282
  // downstream of here, so that field is undefined at this point for BOTH states
7180
7283
  // (the trap that silently disabled #1330's transient branch one block up).
7181
7284
  //
7182
- // NOTHING IS ADOPTED. The probe is the same read-only one (`adopt:false`), so
7183
- // this reports which state it found and the fence moves only on an explicit
7184
- // rebind. The refusal itself is preserved verbatim and the call still fails.
7285
+ // AN UNSTAMPED INERT READ IS ADMITTED by minting a first fence from the
7286
+ // live canvas and retrying once. That is not the retarget #1646 removed:
7287
+ // there is no named workflow to abandon, and a pinned session is left
7288
+ // alone. The panel fence is unchanged (#718). A WRONG stamp still fails.
7289
+ // The probe stays read-only until this branch decides to mint.
7185
7290
  if (isWorkflowInstanceMismatch(err) && isFencedGraphRead(cmd)) {
7186
7291
  const name = typeof cmd.cmd === "string" ? cmd.cmd : "panel command";
7187
7292
  const raw = err instanceof Error ? err.message : String(err);
7188
- const stamped = /issued for workflow instance ([0-9a-f-]{36})/i.exec(raw)?.[1] ?? null;
7189
- const unstamped = /carries no workflow-instance stamp/i.test(raw);
7190
- // Three-valued on purpose. A panel whose wording matches neither is not
7191
- // evidence for either state, and this branch must not manufacture one.
7192
- const shape = unstamped
7193
- ? "unstamped"
7194
- : stamped
7195
- ? "stamped"
7196
- : "unstated";
7293
+ // Anchored to the panel's own clause so a message that quotes BOTH
7294
+ // wordings cannot collapse into the missing-stamp state.
7295
+ const stamped = /this command was issued for workflow instance ([0-9a-f-]{36})/i.exec(raw)?.[1] ?? null;
7296
+ const unstamped = /this command carries no workflow-instance stamp/i.test(raw);
7297
+ // Three-valued on purpose. A panel whose wording matches neither — or
7298
+ // both — is not evidence for either state, and this branch must not
7299
+ // manufacture one.
7300
+ const shape = unstamped === Boolean(stamped) ? "unstated" : unstamped ? "unstamped" : "stamped";
7197
7301
  // Naming `mode:"current"` to a PINNED session is naming something that also
7198
7302
  // RELEASES the pin. Say so where it applies rather than letting the caller
7199
7303
  // discover it by losing their target.
@@ -7203,12 +7307,59 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
7203
7307
  `RELEASES that pin. To keep it, bring that workflow back to the canvas with ` +
7204
7308
  `panel_open_workflow(${JSON.stringify(pin.path)}) and retry instead.`
7205
7309
  : "";
7206
- const RETRY_IS_FREE = `RETRY THIS EXACT CALL ONCE — this is a read, so re-issuing it cannot double-apply ` +
7207
- `anything.`;
7310
+ // graph_run is fenced like a read (the panel refuses it before it queues)
7311
+ // but it is not one: GRAPH_CMD_EFFECT lists it as targeted, and calling
7312
+ // it a read would tell the caller a retry cannot double-apply a prompt.
7313
+ const inertRead = GRAPH_CMD_EFFECT[name] === "inert";
7314
+ const RETRY_IS_FREE = inertRead
7315
+ ? `RETRY THIS EXACT CALL ONCE — this is a read, so re-issuing it cannot double-apply ` +
7316
+ `anything.`
7317
+ : `RETRY THIS EXACT CALL ONCE — the panel refused it before it ran, so re-issuing it ` +
7318
+ `cannot double-apply anything.`;
7319
+ const refusedLead = inertRead
7320
+ ? `${name} was refused before it ran — no graph data was read.`
7321
+ : `${name} was refused before it ran — nothing was applied.`;
7208
7322
  let verdict;
7209
7323
  try {
7210
7324
  const probe = await rebindWorkflowFence(ctx, { adopt: false });
7211
7325
  const live = "uuid" in probe ? probe.uuid : null;
7326
+ const priorAbsent = "before" in probe && probe.before.known === true && !probe.before.uuid;
7327
+ const admitUnstampedRead = inertRead && shape === "unstamped" && pin?.mode !== "pinned";
7328
+ // Mint a first stamp only when this session has none. A fence naming
7329
+ // some other workflow is the retarget #1646 removed; we do not move it.
7330
+ if (admitUnstampedRead &&
7331
+ probe.status === "diverged" &&
7332
+ priorAbsent &&
7333
+ typeof live === "string" &&
7334
+ refreshWorkflowUuid(ctx, { workflow_uuid: live })) {
7335
+ try {
7336
+ const retried = ok(await sendRouted(cmd, timeoutMs, observeRid));
7337
+ if (successProvesSwitchCleared(cmd.cmd))
7338
+ clearSwitchHold(ctx.tabId);
7339
+ return retried;
7340
+ }
7341
+ catch (retryErr) {
7342
+ onFailure?.(retryErr);
7343
+ return fail(retryErr instanceof Error ? retryErr : new Error(String(retryErr)));
7344
+ }
7345
+ }
7346
+ // The fence appeared between dispatch and this probe. Retrying the
7347
+ // inert read now carries that stamp; nothing is adopted here.
7348
+ if (admitUnstampedRead &&
7349
+ (probe.status === "already_current" || probe.status === "healed_by_panel")) {
7350
+ try {
7351
+ const retried = ok(await sendRouted(cmd, timeoutMs, observeRid));
7352
+ if (successProvesSwitchCleared(cmd.cmd))
7353
+ clearSwitchHold(ctx.tabId);
7354
+ return retried;
7355
+ }
7356
+ catch (retryErr) {
7357
+ onFailure?.(retryErr);
7358
+ if (!isWorkflowInstanceMismatch(retryErr)) {
7359
+ return fail(retryErr instanceof Error ? retryErr : new Error(String(retryErr)));
7360
+ }
7361
+ }
7362
+ }
7212
7363
  verdict =
7213
7364
  probe.status === "no_identity"
7214
7365
  // Worded without reference to the session's own side, because this
@@ -7289,7 +7440,7 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
7289
7440
  `refusal stands on its own terms and the fence is unchanged. ` +
7290
7441
  `(${probeErr instanceof Error ? probeErr.message : String(probeErr)})`;
7291
7442
  }
7292
- return fail(`${name} was refused before it ran — no graph data was read. ${raw}${verdict}`);
7443
+ return fail(`${refusedLead} ${raw}${verdict}`);
7293
7444
  }
7294
7445
  // #1480 — NAME A REMEDY THE TAB CAN ACTUALLY ACCEPT.
7295
7446
  //
@@ -9534,7 +9685,7 @@ export function buildPanelToolDefs() {
9534
9685
  node_id: nodeId().describe("Node id whose input to disconnect."),
9535
9686
  input: slotRef.optional().describe("Input slot name or index (default 0)."),
9536
9687
  }, async (args, ctx) => ctx.call({ cmd: "graph_disconnect", node_id: args.node_id, input: args.input })),
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).", {
9688
+ 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). For AnimaRegionalCanvasInline / Krea2RegionalCanvasInline (LC123), quality/scene/red/green/blue/negative prompt writes are refused: the custom textarea and node.properties.animaPrompts overwrite widget.value on APPLY. Drive quality/scene/negative via a PrimitiveStringMultiline wired into quality_prompt_in / scene_prompt_in / negative_prompt_in; red/green/blue have no socket.", {
9538
9689
  node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
9539
9690
  widget: z.string().describe("Widget name (e.g. 'steps', 'cfg', 'text')."),
9540
9691
  value: z
@@ -9557,6 +9708,14 @@ export function buildPanelToolDefs() {
9557
9708
  if (value === undefined) {
9558
9709
  return fail("panel_set_widget needs a `value`. To set an empty string, pass `clear: true` (some clients drop an empty-string `value`).");
9559
9710
  }
9711
+ // #1658: LC123 regional-canvas prompt widgets store the text in a
9712
+ // custom textarea + node.properties.animaPrompts; writing widget.value
9713
+ // reports success and is overwritten on APPLY. Refuse before the write
9714
+ // so the reply is not a lie. Identity is a one-node graph_query; an
9715
+ // unreadable probe is fail-open (some other node may own negative_prompt).
9716
+ const blocked = await refuseAnimaRegionalPromptWrite(ctx, args.node_id, args.widget);
9717
+ if (blocked)
9718
+ return blocked;
9560
9719
  // #599: the frontend runs refresh-before-validate here (pulls a fresh
9561
9720
  // /object_info so a just-staged/-downloaded/-installed value is accepted on
9562
9721
  // a single revalidation, #338/#458) — that authoritative fetch can outlast
@@ -10032,8 +10191,49 @@ export function buildPanelToolDefs() {
10032
10191
  // panel forwarded no `prompt_id` — a later completion can only be reported as
10033
10192
  // UNDETERMINED, so telling the agent to idle and wait would park it on a
10034
10193
  // promise we can't keep. Say so and point at the verification path instead.
10194
+ // #1789 — …AND HONEST ABOUT WHAT "AUTOMATICALLY" RESTS ON. The old text
10195
+ // forbade polling outright on the strength of `correlatable`, which
10196
+ // proves only that a completion COULD be recognised — never that one
10197
+ // will arrive. The producer is the panel's `executed` frame, and a
10198
+ // session that lost it sat idle through a clean 28.77s render until a
10199
+ // human intervened, because the one behaviour that would have caught it
10200
+ // in seconds is the one this rider had banned.
10201
+ //
10202
+ // Delivery is now genuinely stronger (the orchestrator fills in from its
10203
+ // own /history observation — run-completion-watchdog.ts), but it is
10204
+ // still not a guarantee this tool can prove at queue time: the watchdog
10205
+ // sees nothing when the monitored ComfyUI is not the one this run went
10206
+ // to, or cannot be polled. So the anti-poll instruction stays PRIMARY —
10207
+ // it is what keeps an agent from burning a turn a second — and it is
10208
+ // paired with a bounded escape hatch: ONE check, long after the render
10209
+ // should have landed, naming the prompt id so it costs a single call.
10210
+ // A weaker promise with a stated fallback beats a strong one that fails
10211
+ // into an infinite idle.
10212
+ // THE REMEDY MUST BE A CALL THAT ACTUALLY RUNS (gate NO-SHIP on the first
10213
+ // cut of this fix). Naming no id for a batch was right — inventing one
10214
+ // would point the agent at a single arbitrary member — but the remedy it
10215
+ // left behind, `queue (action:"status")` with no id, is REFUSED by the
10216
+ // tool: queue-management.ts's requirePromptId throws on an absent
10217
+ // prompt_id, so the agent that obeyed this fallback got an error instead
10218
+ // of a status. That is the reported bug's own shape, one level in: a
10219
+ // rider promising something the code declines to do. Verified live
10220
+ // against the real handler — `status` + id and bare `list` both return
10221
+ // JSON; bare `status` returns isError with "requires `prompt_id`".
10222
+ //
10223
+ // So the two branches get the call that fits what they can name:
10224
+ // • exactly one id → status on THAT run, the precise answer;
10225
+ // • a batch → list, which takes no id and shows whether any of
10226
+ // them are still in flight (get_history settles the
10227
+ // outcomes once they are not).
10228
+ const fallbackCheck = queuedIds.length === 1
10229
+ ? `queue (action:"status", prompt_id:"${queuedIds[0]}")`
10230
+ : `queue (action:"list")`;
10231
+ const fallbackTail = queuedIds.length === 1
10232
+ ? ""
10233
+ : ` (this run queued ${queuedIds.length} prompts, so the check is the queue as a whole; get_history settles the outcome of any that are no longer in flight)`;
10035
10234
  const note = correlatable
10036
- ? "\n\n[IMPORTANT] You will be notified automatically with the output image(s)/video when the render finishes — do NOT poll queue (action:\"list\"), get_history, or get_image (action:\"list_outputs\"). Just end your turn now and wait for the result to be delivered to you."
10235
+ ? "\n\n[IMPORTANT] You will be notified automatically with the output image(s)/video when the render finishes — do NOT poll queue (action:\"list\"), get_history, or get_image (action:\"list_outputs\"). Just end your turn now and wait for the result to be delivered to you." +
10236
+ `\n[FALLBACK] That notification is journaled and replayed if an agent is not there to take it, and the orchestrator will fill one in from ComfyUI itself — its execution event or its history record — if the panel never reports the finish. But it cannot be GUARANTEED, so do not wait forever on it. If nothing has arrived LONG after this render should have finished (roughly twice the time you expect it to take), make ONE check with ${fallbackCheck} instead of idling indefinitely${fallbackTail}. One check, not a loop — and not before then.`
10037
10237
  : "\n\n[IMPORTANT] The run was queued, but the panel reported NO prompt id for it, so a completion event CANNOT be correlated back to this run — its outcome will be reported to you as UNDETERMINED. Do NOT simply idle and wait indefinitely: end your turn, and if nothing arrives, confirm the outcome with get_history (action:\"list\") before acting on it.";
10038
10238
  // Backpressure note. A backlog is only alarming when it's a job we did NOT
10039
10239
  // queue (possibly foreign/stuck). Deliberately batching renders — a sweep,