comfyui-mcp 0.50.60 → 0.50.62

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.
@@ -1313,6 +1313,95 @@ function withTruncationHints(res, rules) {
1313
1313
  * the bound this tool advertises did not apply. The flag is stripped again before the
1314
1314
  * result is returned — it exists only to drive the rider.
1315
1315
  */
1316
+ /**
1317
+ * A 0-node outline is a CLAIM, and one made mid-restore is not established (#1184).
1318
+ *
1319
+ * Right after a ComfyUI restart and a tab switch, graph_outline returned
1320
+ * `node_count: 0` for a tab holding a 7-node starter graph — the frontend was
1321
+ * still restoring, and the canvas transiently had nothing on it. The agent
1322
+ * trusted the empty read, reported the canvas empty, and BUILT A NEW 9-NODE
1323
+ * PIPELINE alongside the invisible one. A later panel_query_graph showed 16
1324
+ * nodes: ids 1–7 had been there the whole time, which the frontend knew, because
1325
+ * the new adds started at id 8.
1326
+ *
1327
+ * That is the worst shape of this defect class, because the action taken on the
1328
+ * false read DESTROYS WORK: a duplicate pipeline cannot be un-built, and the user
1329
+ * is left with two overlapping graphs and no way to tell which nodes are theirs.
1330
+ *
1331
+ * It is also unusually cheap to get right. Unlike most "could not determine"
1332
+ * cases, an empty outline is trivially RE-VERIFIABLE: read it again, and the
1333
+ * restore has either finished or it has not.
1334
+ *
1335
+ * So an empty first read is re-read once after a short settle:
1336
+ * - second read non-empty → the first was a race; report the real graph.
1337
+ * - second read still empty → "empty" is now OBSERVED TWICE, not assumed, and
1338
+ * is reported plainly with no hedge (a blank canvas is a normal state and
1339
+ * must not be narrated as suspicious).
1340
+ *
1341
+ * The whole cost lands on the genuinely-empty case — one extra cheap read — which
1342
+ * is the right place for it: a blank canvas is common but harmless to re-check,
1343
+ * while a false empty is rare and expensive.
1344
+ *
1345
+ * Never throws: a failed re-read leaves the original reply exactly as it was.
1346
+ */
1347
+ async function confirmEmptyOutline(ctx, res, reread) {
1348
+ try {
1349
+ if (res.isError)
1350
+ return res;
1351
+ const first = parseToolResultJson(res);
1352
+ if (!first || first.node_count !== 0)
1353
+ return res;
1354
+ // Which workflow this empty answer is ABOUT (codex review). The re-read is a
1355
+ // second round trip, and the user can switch tabs during it — so a non-empty
1356
+ // second outline might describe a DIFFERENT workflow entirely. Substituting
1357
+ // it would report workflow B's graph as though it confirmed a read of A,
1358
+ // which is a worse failure than the empty read this exists to fix.
1359
+ const identityBefore = currentWorkflowFence(ctx);
1360
+ // A BOUNDED POLL, not a single retry (codex review). A restore after a
1361
+ // ComfyUI restart has no guaranteed duration, and one 400ms attempt is an
1362
+ // arbitrary cliff: if the restore is still going at that instant, the fix
1363
+ // silently fails to cover the very report it is for. Poll briefly instead,
1364
+ // stopping the moment nodes appear.
1365
+ for (const waitMs of EMPTY_OUTLINE_RECHECK_STEPS_MS) {
1366
+ await sleep(waitMs);
1367
+ const second = await reread();
1368
+ // Redundant with the parsed-null guard below TODAY — an error result carries
1369
+ // no parseable JSON — and kept anyway: it states the intent directly, and
1370
+ // would still hold if an error result ever carried a structured body.
1371
+ if (second.isError)
1372
+ continue;
1373
+ const parsed = parseToolResultJson(second);
1374
+ if (!parsed || parsed.node_count === 0)
1375
+ continue;
1376
+ // Nodes appeared — but only adopt them if the canvas is still the SAME
1377
+ // workflow. A changed identity means the second read answers a different
1378
+ // question, so the original (honest, empty) answer stands.
1379
+ const identityAfter = currentWorkflowFence(ctx);
1380
+ if (identityBefore.known &&
1381
+ identityAfter.known &&
1382
+ identityBefore.uuid !== identityAfter.uuid) {
1383
+ return res;
1384
+ }
1385
+ return second;
1386
+ }
1387
+ return res;
1388
+ }
1389
+ catch {
1390
+ return res;
1391
+ }
1392
+ }
1393
+ /**
1394
+ * The back-off schedule for re-reading an empty outline (#1184).
1395
+ *
1396
+ * A bounded poll rather than one attempt: a restore after a ComfyUI restart has
1397
+ * no guaranteed duration, so a single fixed wait is an arbitrary cliff that would
1398
+ * silently miss a slower restore (codex review). Stops the moment nodes appear.
1399
+ *
1400
+ * Bounded deliberately at ~1.2s total. A genuinely blank canvas is a COMMON
1401
+ * state and pays this whole cost, so it has to stay small enough not to be felt;
1402
+ * a longer poll would buy rarer restores at the expense of every empty read.
1403
+ */
1404
+ const EMPTY_OUTLINE_RECHECK_STEPS_MS = [250, 400, 550];
1316
1405
  function markBudgetIgnored(res, requested) {
1317
1406
  if (typeof requested !== "number")
1318
1407
  return res;
@@ -5647,7 +5736,9 @@ export function buildPanelToolDefs() {
5647
5736
  }, async (args, ctx) => withTruncationHints(
5648
5737
  // The synthetic `__budget_ignored` flag below is derived from the reply, not
5649
5738
  // sent by the panel: a build that supports the budget echoes `max_chars` back.
5650
- markBudgetIgnored(await ctx.call({ cmd: "graph_outline", max_chars: args.max_chars }), args.max_chars), [
5739
+ markBudgetIgnored(
5740
+ // #1184 — an empty outline is re-verified before it is believed.
5741
+ await confirmEmptyOutline(ctx, await ctx.call({ cmd: "graph_outline", max_chars: args.max_chars }), () => ctx.call({ cmd: "graph_outline", max_chars: args.max_chars })), args.max_chars), [
5651
5742
  {
5652
5743
  // #809 (codex gate): a panel older than this budget IGNORES `max_chars` and
5653
5744
  // returns the full outline, so the bound this tool advertises silently did