comfyui-mcp 0.43.0 → 0.44.0

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 (57) hide show
  1. package/README.md +8 -0
  2. package/dist/config.js +42 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/orchestrator/agent-backend.js +24 -10
  5. package/dist/orchestrator/agent-backend.js.map +1 -1
  6. package/dist/orchestrator/antigravity-backend.js +575 -0
  7. package/dist/orchestrator/antigravity-backend.js.map +1 -0
  8. package/dist/orchestrator/backend-readiness.js +23 -14
  9. package/dist/orchestrator/backend-readiness.js.map +1 -1
  10. package/dist/orchestrator/copilot-backend.js +3 -2
  11. package/dist/orchestrator/copilot-backend.js.map +1 -1
  12. package/dist/orchestrator/index.js +267 -147
  13. package/dist/orchestrator/index.js.map +1 -1
  14. package/dist/orchestrator/panel-agent.js +138 -18
  15. package/dist/orchestrator/panel-agent.js.map +1 -1
  16. package/dist/orchestrator/panel-console-http.js +4 -3
  17. package/dist/orchestrator/panel-console-http.js.map +1 -1
  18. package/dist/orchestrator/start-failure-notice.js +63 -0
  19. package/dist/orchestrator/start-failure-notice.js.map +1 -0
  20. package/dist/services/code-provider-auth.js +49 -14
  21. package/dist/services/code-provider-auth.js.map +1 -1
  22. package/dist/services/job-history.js +50 -0
  23. package/dist/services/job-history.js.map +1 -1
  24. package/dist/services/job-watcher.js +2 -0
  25. package/dist/services/job-watcher.js.map +1 -1
  26. package/dist/services/openai-provider-registry.js +87 -0
  27. package/dist/services/openai-provider-registry.js.map +1 -0
  28. package/dist/services/panel-secrets.js +167 -59
  29. package/dist/services/panel-secrets.js.map +1 -1
  30. package/dist/services/queue-manager.js +2 -1
  31. package/dist/services/queue-manager.js.map +1 -1
  32. package/dist/services/queue-monitor.js +241 -7
  33. package/dist/services/queue-monitor.js.map +1 -1
  34. package/dist/services/queue-monitor.poll.test.js +267 -0
  35. package/dist/services/queue-monitor.poll.test.js.map +1 -0
  36. package/dist/services/queue-status-broadcast.js +26 -9
  37. package/dist/services/queue-status-broadcast.js.map +1 -1
  38. package/dist/services/queue-status-broadcast.test.js +49 -0
  39. package/dist/services/queue-status-broadcast.test.js.map +1 -1
  40. package/dist/services/runpod-client.js +190 -0
  41. package/dist/services/runpod-client.js.map +1 -0
  42. package/dist/services/runpod-watch.js +174 -0
  43. package/dist/services/runpod-watch.js.map +1 -0
  44. package/dist/tools/diagnostics.js +54 -27
  45. package/dist/tools/diagnostics.js.map +1 -1
  46. package/dist/tools/index.js +2 -0
  47. package/dist/tools/index.js.map +1 -1
  48. package/dist/tools/queue-management.js +1 -1
  49. package/dist/tools/queue-management.js.map +1 -1
  50. package/dist/tools/runpod.js +285 -0
  51. package/dist/tools/runpod.js.map +1 -0
  52. package/package.json +1 -1
  53. package/plugin/skills/ltx-director/SKILL.md +174 -0
  54. package/dist/orchestrator/glm-backend.js +0 -20
  55. package/dist/orchestrator/glm-backend.js.map +0 -1
  56. package/dist/orchestrator/moonshot-backend.js +0 -22
  57. package/dist/orchestrator/moonshot-backend.js.map +0 -1
@@ -32,26 +32,29 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
32
32
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
33
33
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
34
34
  import { registerAllTools } from "../tools/index.js";
35
- import { isForceRemoteFlagSet, isLoopbackHost, detectLocalComfyUIPath, setComfyuiTarget } from "../config.js";
35
+ import { isForceRemoteFlagSet, isLoopbackHost, detectLocalComfyUIPath, setComfyuiTarget, onComfyuiTargetChanged, isTargetingLocal, getComfyUIBaseUrl } from "../config.js";
36
36
  import { buildComfyuiMcpEnv, comfyuiSecretKeys, onComfyuiSecretsChanged, hydrateAgentSecretsIntoEnv, onAgentSecretsChanged, setAgentSecret, setComfyuiSecret, isAllowedComfyuiSecretKey, } from "../services/panel-secrets.js";
37
37
  import { CodexBackend } from "./codex-backend.js";
38
38
  import { GeminiBackend, GEMINI_DEFAULT_MODEL } from "./gemini-backend.js";
39
+ import { AntigravityBackend } from "./antigravity-backend.js";
39
40
  import { GrokBackend, GROK_DEFAULT_MODEL } from "./grok-backend.js";
40
41
  import { OllamaBackend, OLLAMA_SYSTEM_PROMPT } from "./ollama-backend.js";
41
42
  import { ChatGptOAuthBackend, CHATGPT_DEFAULT_MODEL } from "./chatgpt-oauth-backend.js";
42
- import { GlmBackend, GLM_DEFAULT_MODEL } from "./glm-backend.js";
43
- import { KimiBackend, KIMI_DEFAULT_MODEL } from "./kimi-backend.js";
44
- import { MoonshotBackend, MOONSHOT_DEFAULT_MODEL } from "./moonshot-backend.js";
43
+ import { KimiBackend } from "./kimi-backend.js";
44
+ import { OPENAI_KEY_PROVIDER_IDS, openAiKeyProvider, simpleKeyProvider, openAiKeyProviderModel, } from "../services/openai-provider-registry.js";
45
+ import { resolveOpenAiKeyCredentials } from "../services/code-provider-auth.js";
45
46
  import { CopilotBackend, COPILOT_DEFAULT_MODEL } from "./copilot-backend.js";
46
47
  import { SYSTEM as MODEL_CARD_SYSTEM } from "./ai-proposer.js";
47
48
  import { resolvePrompt, registerPrompt, onPromptsChanged } from "../services/prompt-overrides.js";
48
49
  import { allBackendReadiness } from "./backend-readiness.js";
49
50
  import { handleOAuthBegin, handleOAuthStatus, handleOAuthSignout } from "./oauth-bridge.js";
51
+ import { buildStartFailureNotice } from "./start-failure-notice.js";
50
52
  import { OAUTH_PROVIDERS } from "../services/oauth-flow.js";
51
53
  import { startPanelMcpHttpServer } from "./panel-mcp-http.js";
52
54
  import { startPanelConsoleHttpServer } from "./panel-console-http.js";
53
55
  import { readComfyuiCrashLog, formatCrashNote } from "../services/crash-log.js";
54
56
  import { QueueMonitor } from "../services/queue-monitor.js";
57
+ import { initRunpodWatcher, getRunpodWatcher } from "../services/runpod-watch.js";
55
58
  import { buildQueueStatusFrame, createQueueStatusBroadcaster, } from "../services/queue-status-broadcast.js";
56
59
  import { unloadAllOllama, warmOllama, resolveOllamaHost } from "../services/ollama-vram.js";
57
60
  import { isLocalLmstudio, startLmstudioServer, unloadAllLmstudio, warmLmstudio, } from "../services/lmstudio-lifecycle.js";
@@ -170,9 +173,10 @@ const HEADLESS_DIRECTIVE = "[HEADLESS SESSION — no ComfyUI panel/canvas is con
170
173
  "enqueuing returns a prompt_id immediately, so wait for it with get_job_status(prompt_id) — poll it briefly until " +
171
174
  "it reports completion (this is the ONE case where polling IS correct) — then fetch the output with get_history and " +
172
175
  "show it with panel_show_media. Do NOT end your turn expecting an automatic notification; none will arrive. " +
173
- "If the run FAILED — or the user asks why a render failed / what's missing — call diagnose_run: it names the failed " +
174
- "node with its exception, plus any missing models (exact file + the widget holding it) and missing node types, in one " +
175
- "call. It is the canvas-less equivalent of the panel's \"why is this red?\", so do NOT try panel_view_errored_nodes here.";
176
+ "If the run FAILED — or the user asks why a render failed / what's missing — call diagnose_run FIRST, and do NOT use " +
177
+ "get_history for that: diagnose_run returns everything get_history would (failed node + exception + traceback) PLUS the " +
178
+ "missing models (exact file + the widget holding it) and missing node types that get_history omits, in one call. It is " +
179
+ "the canvas-less equivalent of the panel's \"why is this red?\", so also do NOT try panel_view_errored_nodes here.";
176
180
  /** Live stall threshold (seconds) pushed from the panel setting via a `set_config`
177
181
  * frame — applies WITHOUT a reconnect. null = not set, fall back to env then the
178
182
  * built-in default. Process-global: one ComfyUI per orchestrator. */
@@ -597,6 +601,22 @@ const CALL_TOOL_WHITELIST = new Set([
597
601
  "train_prepare_dataset",
598
602
  "train_start",
599
603
  "train_cancel",
604
+ // RunPod control panel (desktop + mobile): the one-tap pod lifecycle + the
605
+ // local⇄pod host switch. Read-only status/list/troubleshoot plus the
606
+ // user-initiated lifecycle actions (create/start/stop/connect/use_local/
607
+ // watch/unwatch) and the referral deploy link. Each tool validates its own
608
+ // pod state; the whitelist only gates reachability from a canvas-less client.
609
+ "runpod_pod_status",
610
+ "runpod_list_pods",
611
+ "runpod_pod_start",
612
+ "runpod_pod_stop",
613
+ "runpod_pod_create",
614
+ "runpod_pod_connect",
615
+ "runpod_pod_troubleshoot",
616
+ "runpod_use_local",
617
+ "runpod_watch",
618
+ "runpod_unwatch",
619
+ "runpod_deploy_link",
600
620
  ]);
601
621
  /** Lazily build ONE in-process MCP client wired to the full comfyui tool surface,
602
622
  * reused across call_tool requests. Reuses the exact tool implementations (same
@@ -974,6 +994,9 @@ export async function runPanelOrchestrator() {
974
994
  // COMFYUI_MCP_GEMINI_MODEL (default gemini-2.5-pro). The model is applied at spawn
975
995
  // via the CLI `--model` flag (ACP exposes no per-session model setter).
976
996
  const geminiModel = process.env.COMFYUI_MCP_GEMINI_MODEL ?? GEMINI_DEFAULT_MODEL;
997
+ // Antigravity (`agy`, issue #262): no default on purpose — unset means the
998
+ // account's own default model; the live catalog comes from `agy models`.
999
+ const antigravityModel = process.env.COMFYUI_MCP_ANTIGRAVITY_MODEL;
977
1000
  const grokModel = process.env.COMFYUI_MCP_GROK_MODEL ?? GROK_DEFAULT_MODEL;
978
1001
  // Ollama (local LLMs, issue #97): the model is a local tag applied PER
979
1002
  // REQUEST — switching live is free. Default = OUR FINE-TUNE,
@@ -1001,14 +1024,18 @@ export async function runPanelOrchestrator() {
1001
1024
  {
1002
1025
  const hydrated = new Set(hydratedSecrets);
1003
1026
  const src = (k) => (process.env[k] ? (hydrated.has(k) ? "store" : "env") : "none");
1004
- logger.info(`[panel-orchestrator] keyed providers: openrouter=${src("OPENROUTER_API_KEY")}, glm=${src("GLM_API_KEY")}, kimi=${src("KIMI_API_KEY")}, moonshot=${src("MOONSHOT_API_KEY")}`);
1027
+ const registryKeyed = OPENAI_KEY_PROVIDER_IDS.map((id) => `${id}=${src(openAiKeyProvider(id).envKeys[0])}`).join(", ");
1028
+ logger.info(`[panel-orchestrator] keyed providers: openrouter=${src("OPENROUTER_API_KEY")}, ${registryKeyed}`);
1005
1029
  }
1006
1030
  const persistedAgent = getAgentSettings();
1007
1031
  let ollamaModel = process.env.COMFYUI_MCP_OLLAMA_MODEL ?? persistedAgent.ollama?.model ?? "artokun/gemma4-comfyui-mcp:e4b";
1008
1032
  const chatgptModel = process.env.COMFYUI_MCP_CHATGPT_MODEL ?? CHATGPT_DEFAULT_MODEL;
1009
- const glmModel = process.env.COMFYUI_MCP_GLM_MODEL ?? GLM_DEFAULT_MODEL;
1010
- const kimiModel = process.env.COMFYUI_MCP_KIMI_MODEL ?? KIMI_DEFAULT_MODEL;
1011
- const moonshotModel = process.env.COMFYUI_MCP_MOONSHOT_MODEL ?? MOONSHOT_DEFAULT_MODEL;
1033
+ // kimi keeps a local model var (its bespoke KimiBackend takes it). glm/moonshot
1034
+ // read their model straight from the registry via the shared factory, so no
1035
+ // per-provider model var is needed for them anymore. All three still resolve to
1036
+ // the exact value the old <provider>Model consts held
1037
+ // (process.env.COMFYUI_MCP_<X>_MODEL ?? <X>_DEFAULT_MODEL).
1038
+ const kimiModel = openAiKeyProviderModel(openAiKeyProvider("kimi"));
1012
1039
  // EXPERIMENTAL (ToS risk) — off by default, only reachable once the user has
1013
1040
  // signed in via the panel's experimental row (oauth-bridge.ts's
1014
1041
  // allow_experimental gate); never the defaultBackend/auto-pick.
@@ -1096,10 +1123,10 @@ export async function runPanelOrchestrator() {
1096
1123
  "codex",
1097
1124
  "chatgpt",
1098
1125
  "gemini",
1126
+ "antigravity",
1099
1127
  "grok",
1100
- "glm",
1101
- "kimi",
1102
- "moonshot",
1128
+ // Simple api-key providers (glm/kimi/moonshot) come from the registry.
1129
+ ...OPENAI_KEY_PROVIDER_IDS,
1103
1130
  "ollama",
1104
1131
  "openrouter",
1105
1132
  "lmstudio",
@@ -1283,6 +1310,23 @@ export async function runPanelOrchestrator() {
1283
1310
  },
1284
1311
  close: async () => { },
1285
1312
  });
1313
+ // ONE code path for the simple OpenAI-compatible api-key providers (glm,
1314
+ // moonshot, …): resolve the provider's key + base URL (throws if absent), then
1315
+ // build the shared OllamaBackend openai driver pinned to that host/key/model.
1316
+ // Collapses the near-identical GlmBackend/MoonshotBackend classes. `kimi` is
1317
+ // excluded (simpleKeyProvider filters it) — its OAuth prepare() keeps KimiBackend.
1318
+ const makeOpenAiKeyBackend = (reg, extra = {}) => {
1319
+ const creds = resolveOpenAiKeyCredentials(reg.id);
1320
+ return new OllamaBackend({
1321
+ ...extra,
1322
+ cwd: comfyuiPath ?? process.cwd(),
1323
+ backendId: reg.id,
1324
+ model: openAiKeyProviderModel(reg),
1325
+ api: "openai",
1326
+ host: creds.baseUrl,
1327
+ apiKey: creds.apiKey,
1328
+ });
1329
+ };
1286
1330
  const makeBackend = (key) => {
1287
1331
  const backend = backendOf(key);
1288
1332
  const panelTabId = panelTabOf(key);
@@ -1305,6 +1349,14 @@ export async function runPanelOrchestrator() {
1305
1349
  mcpServers: makeHttpBackendMcpServers(panelTabId),
1306
1350
  });
1307
1351
  }
1352
+ if (backend === "antigravity") {
1353
+ return new AntigravityBackend({
1354
+ cwd: comfyuiPath ?? process.cwd(),
1355
+ ...(antigravityModel ? { model: antigravityModel } : {}),
1356
+ systemAppend: panelSystemAppend,
1357
+ mcpServers: makeHttpBackendMcpServers(panelTabId),
1358
+ });
1359
+ }
1308
1360
  if (backend === "grok") {
1309
1361
  return new GrokBackend({
1310
1362
  cwd: comfyuiPath ?? process.cwd(),
@@ -1373,10 +1425,10 @@ export async function runPanelOrchestrator() {
1373
1425
  mcpServers: makeHttpBackendMcpServers(panelTabId),
1374
1426
  });
1375
1427
  }
1376
- if (backend === "glm") {
1377
- return new GlmBackend({
1378
- cwd: comfyuiPath ?? process.cwd(),
1379
- model: glmModel,
1428
+ const simpleKeyReg = simpleKeyProvider(backend);
1429
+ if (simpleKeyReg) {
1430
+ // glm/moonshot (and any future simple api-key provider) share one factory.
1431
+ return makeOpenAiKeyBackend(simpleKeyReg, {
1380
1432
  systemAppend: panelSystemAppend,
1381
1433
  comfyuiUrl,
1382
1434
  mcpServers: makeHttpBackendMcpServers(panelTabId),
@@ -1391,15 +1443,6 @@ export async function runPanelOrchestrator() {
1391
1443
  mcpServers: makeHttpBackendMcpServers(panelTabId),
1392
1444
  });
1393
1445
  }
1394
- if (backend === "moonshot") {
1395
- return new MoonshotBackend({
1396
- cwd: comfyuiPath ?? process.cwd(),
1397
- model: moonshotModel,
1398
- systemAppend: panelSystemAppend,
1399
- comfyuiUrl,
1400
- mcpServers: makeHttpBackendMcpServers(panelTabId),
1401
- });
1402
- }
1403
1446
  if (backend === "copilot") {
1404
1447
  // EXPERIMENTAL (ToS risk) — prepare() throws a clear re-sign-in error if
1405
1448
  // no ~/.comfyui-mcp/copilot-auth.json exists yet (resolveCopilotOAuth),
@@ -1435,32 +1478,35 @@ export async function runPanelOrchestrator() {
1435
1478
  let pb = probeBackends.get(backend);
1436
1479
  if (!pb) {
1437
1480
  try {
1438
- pb =
1439
- backend === "codex"
1481
+ const simpleKeyReg = simpleKeyProvider(backend);
1482
+ pb = simpleKeyReg
1483
+ ? makeOpenAiKeyBackend(simpleKeyReg)
1484
+ : backend === "codex"
1440
1485
  ? new CodexBackend({ cwd: comfyuiPath ?? process.cwd(), model: codexModel })
1441
1486
  : backend === "chatgpt"
1442
1487
  ? new ChatGptOAuthBackend({ cwd: comfyuiPath ?? process.cwd(), model: chatgptModel })
1443
- : backend === "glm"
1444
- ? new GlmBackend({ cwd: comfyuiPath ?? process.cwd(), model: glmModel })
1445
- : backend === "kimi"
1446
- ? new KimiBackend({ cwd: comfyuiPath ?? process.cwd(), model: kimiModel })
1447
- : backend === "moonshot"
1448
- ? new MoonshotBackend({ cwd: comfyuiPath ?? process.cwd(), model: moonshotModel })
1449
- : backend === "ollama"
1450
- ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: ollamaModel, ...ollamaDeps() })
1451
- : backend === "grok"
1452
- ? new GrokBackend({ cwd: comfyuiPath ?? process.cwd(), model: grokModel })
1453
- : backend === "openrouter"
1454
- ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: openrouterModel, ...openrouterDeps() })
1455
- : backend === "lmstudio"
1456
- ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: lmstudioModel, ...lmstudioDeps() })
1457
- : backend === "llamacpp"
1458
- ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: llamacppModel, ...llamacppDeps() })
1459
- : backend === "custom"
1460
- ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: customModel, ...customDeps() })
1461
- : backend === "copilot"
1462
- ? new CopilotBackend({ cwd: comfyuiPath ?? process.cwd(), model: copilotModel })
1463
- : new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
1488
+ : backend === "kimi"
1489
+ ? new KimiBackend({ cwd: comfyuiPath ?? process.cwd(), model: kimiModel })
1490
+ : backend === "ollama"
1491
+ ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: ollamaModel, ...ollamaDeps() })
1492
+ : backend === "antigravity"
1493
+ ? new AntigravityBackend({
1494
+ cwd: comfyuiPath ?? process.cwd(),
1495
+ ...(antigravityModel ? { model: antigravityModel } : {}),
1496
+ })
1497
+ : backend === "grok"
1498
+ ? new GrokBackend({ cwd: comfyuiPath ?? process.cwd(), model: grokModel })
1499
+ : backend === "openrouter"
1500
+ ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: openrouterModel, ...openrouterDeps() })
1501
+ : backend === "lmstudio"
1502
+ ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: lmstudioModel, ...lmstudioDeps() })
1503
+ : backend === "llamacpp"
1504
+ ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: llamacppModel, ...llamacppDeps() })
1505
+ : backend === "custom"
1506
+ ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: customModel, ...customDeps() })
1507
+ : backend === "copilot"
1508
+ ? new CopilotBackend({ cwd: comfyuiPath ?? process.cwd(), model: copilotModel })
1509
+ : new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
1464
1510
  probeBackends.set(backend, pb);
1465
1511
  }
1466
1512
  catch (err) {
@@ -1562,13 +1608,34 @@ export async function runPanelOrchestrator() {
1562
1608
  onSeen: (key, mid) => {
1563
1609
  bridge.push({ type: "ack", ok: true, kind: "seen", mid }, panelTabOf(key));
1564
1610
  },
1611
+ // PER-TAB start failure (issue #250): a backend that rejects at
1612
+ // prepare()/first-connect — an invalid API key 401ing on an OpenAI-dialect
1613
+ // provider (moonshot/glm/custom/openrouter), an unreachable endpoint — is a
1614
+ // tab-local configuration error, the same class as the keyless ctor path
1615
+ // (#209), one step later. Degrade THAT tab only: an honest say naming the
1616
+ // provider with check-your-key guidance, plus a degraded ack so the panel
1617
+ // shows the real state. The manager already dropped the dead agent, so
1618
+ // fixing the key and Disconnect → Connect (or just re-sending) retries
1619
+ // cleanly. This must NOT self-exit — a bad moonshot key on one tab was
1620
+ // killing healthy sessions on every other tab.
1621
+ onStartFailure: (key, message) => {
1622
+ // Frame construction (hint selection via the key-provider registry,
1623
+ // composite-key → panel-tab split, say + degraded ack + turn:done) lives
1624
+ // in start-failure-notice.ts so it is unit-testable (issue #255).
1625
+ const { panelTab, backend, frames } = buildStartFailureNotice(key, message, defaultBackend);
1626
+ for (const frame of frames)
1627
+ bridge.push(frame, panelTab);
1628
+ logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} (${backend}) agent failed to start — degraded THIS tab only, other tabs unaffected (${message})`);
1629
+ },
1565
1630
  // ROOT-CAUSE self-exit (the "bridge open but no panel agent responded" wedge):
1566
- // a tab's agent died fatally (couldn't start, or its bounded self-restart gave
1567
- // up). The orchestrator is alive and the bridge is up, but no agent will ever
1568
- // handshake — exactly the wedge. Exit cleanly so the panel pack's bridge-death
1569
- // → reclaim + sticky-reconnect respawns a FRESH orchestrator, instead of
1570
- // leaving the user staring at the manual "fully restart ComfyUI" warning.
1571
- // Mirrors the uncaughtException exit above (Node's own default on a fatal).
1631
+ // a tab's agent died fatally — its bounded self-restart loop gave up (the
1632
+ // session kept dropping immediately). The orchestrator is alive and the
1633
+ // bridge is up, but no agent will ever handshake — exactly the wedge. Exit
1634
+ // cleanly so the panel pack's bridge-death → reclaim + sticky-reconnect
1635
+ // respawns a FRESH orchestrator, instead of leaving the user staring at the
1636
+ // manual "fully restart ComfyUI" warning. Mirrors the uncaughtException exit
1637
+ // above (Node's own default on a fatal). Start failures no longer route here
1638
+ // (issue #250) — they degrade per-tab via onStartFailure above.
1572
1639
  onAgentFatal: (tabId, reason) => {
1573
1640
  requestSelfExit(`tab ${tabId.slice(0, 8)} ${reason}`);
1574
1641
  },
@@ -1822,6 +1889,8 @@ export async function runPanelOrchestrator() {
1822
1889
  return codexModel;
1823
1890
  if (backend === "gemini")
1824
1891
  return geminiModel;
1892
+ if (backend === "antigravity")
1893
+ return antigravityModel;
1825
1894
  if (backend === "grok")
1826
1895
  return grokModel;
1827
1896
  if (backend === "ollama")
@@ -1836,12 +1905,9 @@ export async function runPanelOrchestrator() {
1836
1905
  return customModel || undefined;
1837
1906
  if (backend === "chatgpt")
1838
1907
  return chatgptModel;
1839
- if (backend === "glm")
1840
- return glmModel;
1841
- if (backend === "kimi")
1842
- return kimiModel;
1843
- if (backend === "moonshot")
1844
- return moonshotModel;
1908
+ const reg = openAiKeyProvider(backend);
1909
+ if (reg)
1910
+ return openAiKeyProviderModel(reg); // glm / kimi / moonshot
1845
1911
  if (backend === "copilot")
1846
1912
  return copilotModel;
1847
1913
  return model;
@@ -2061,6 +2127,13 @@ export async function runPanelOrchestrator() {
2061
2127
  // are change-only, so a tab connecting MID-render would otherwise wait for
2062
2128
  // the next state transition to learn a job is already running.
2063
2129
  bridge.push(buildQueueStatusFrame(QueueMonitor.snapshot()), panelTab);
2130
+ // Seed the RunPod control panel too — a tab that just connected gets the
2131
+ // current pod-status frame (or a cleared one when nothing is watched).
2132
+ const rpFrame = getRunpodWatcher()?.current();
2133
+ if (rpFrame)
2134
+ bridge.push(rpFrame, panelTab);
2135
+ // Seed the honest host indicator: tell this tab where renders run now.
2136
+ bridge.push({ type: "comfyui_target", url: getComfyUIBaseUrl(), is_local: isTargetingLocal() }, panelTab);
2064
2137
  // Re-push the last usage so the context meter isn't blank after a reload.
2065
2138
  const lastStatus = manager.lastStatusFor(key);
2066
2139
  if (lastStatus)
@@ -2072,10 +2145,10 @@ export async function runPanelOrchestrator() {
2072
2145
  const isCx = backend === "codex";
2073
2146
  const isCg = backend === "chatgpt";
2074
2147
  const isGm = backend === "gemini";
2148
+ const isAg = backend === "antigravity";
2075
2149
  const isGk = backend === "grok";
2076
- const isGl = backend === "glm";
2077
- const isKm = backend === "kimi";
2078
- const isMs = backend === "moonshot";
2150
+ // glm/kimi/moonshot share one registry-driven ack (label + ready + degraded).
2151
+ const reg = openAiKeyProvider(backend);
2079
2152
  const isOl = backend === "ollama";
2080
2153
  const isOr = backend === "openrouter";
2081
2154
  const isLs = backend === "lmstudio";
@@ -2116,33 +2189,31 @@ export async function runPanelOrchestrator() {
2116
2189
  : ensureModels(backend))
2117
2190
  .then((models) => {
2118
2191
  if (models.length) {
2119
- const agentLabel = isCx
2120
- ? (codexModel ?? models[0].value ?? "Codex")
2121
- : isCg
2122
- ? (chatgptModel ?? models[0].value ?? "ChatGPT")
2123
- : isGm
2124
- ? (geminiModel ?? models[0].value ?? "Gemini")
2125
- : isGk
2126
- ? (grokModel ?? models[0].value ?? "Grok")
2127
- : isGl
2128
- ? (glmModel ?? models[0].value ?? "GLM")
2129
- : isKm
2130
- ? (kimiModel ?? models[0].value ?? "Kimi")
2131
- : isMs
2132
- ? (moonshotModel ?? models[0].value ?? "Kimi K3")
2133
- : isOl
2134
- ? (ollamaModel ?? models[0].value ?? "Ollama")
2135
- : isLs
2136
- ? (lmstudioModel || (models[0].value ?? "LM Studio"))
2137
- : isLc
2138
- ? (llamacppModel || (models[0].value ?? "llama.cpp"))
2139
- : isCu
2140
- ? (customModel || (models[0].value ?? "Custom endpoint"))
2141
- : isOr
2142
- ? (openrouterModel ?? models[0].value ?? "OpenRouter")
2143
- : isCp
2144
- ? (copilotModel ?? models[0].value ?? "Copilot")
2145
- : model;
2192
+ const agentLabel = reg
2193
+ ? (openAiKeyProviderModel(reg) ?? models[0].value ?? reg.ackFallbackLabel)
2194
+ : isCx
2195
+ ? (codexModel ?? models[0].value ?? "Codex")
2196
+ : isCg
2197
+ ? (chatgptModel ?? models[0].value ?? "ChatGPT")
2198
+ : isGm
2199
+ ? (geminiModel ?? models[0].value ?? "Gemini")
2200
+ : isAg
2201
+ ? (antigravityModel ?? models[0].value ?? "Antigravity")
2202
+ : isGk
2203
+ ? (grokModel ?? models[0].value ?? "Grok")
2204
+ : isOl
2205
+ ? (ollamaModel ?? models[0].value ?? "Ollama")
2206
+ : isLs
2207
+ ? (lmstudioModel || (models[0].value ?? "LM Studio"))
2208
+ : isLc
2209
+ ? (llamacppModel || (models[0].value ?? "llama.cpp"))
2210
+ : isCu
2211
+ ? (customModel || (models[0].value ?? "Custom endpoint"))
2212
+ : isOr
2213
+ ? (openrouterModel ?? models[0].value ?? "OpenRouter")
2214
+ : isCp
2215
+ ? (copilotModel ?? models[0].value ?? "Copilot")
2216
+ : model;
2146
2217
  // llama.cpp launch gotchas (issue #161): a reachable server can still
2147
2218
  // be useless for us — tool calling needs --jinja (rejected requests),
2148
2219
  // and a launch-time -c under ~16K silently truncates the tool payload.
@@ -2172,64 +2243,60 @@ export async function runPanelOrchestrator() {
2172
2243
  }
2173
2244
  // Greet only on a FRESH session (a resume/reconnect already has the thread).
2174
2245
  if (!resume) {
2175
- const readyText = isCx
2176
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Codex (ChatGPT) account. Ask away.`
2177
- : isCg
2178
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your ChatGPT subscription (direct OAuth). Ask away.`
2179
- : isGm
2180
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google account (Gemini Code Assist). Ask away.`
2181
- : isGk
2182
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Grok (xAI) account. Ask away.`
2183
- : isGl
2184
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Z.AI GLM Coding Plan. Ask away.`
2185
- : isKm
2186
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Kimi Code subscription. Ask away.`
2187
- : isMs
2188
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Moonshot platform (Kimi K3) API key. Ask away.`
2189
- : isOl
2190
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via Ollama (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
2191
- : isLs
2192
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via LM Studio (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
2193
- : isLc
2194
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via llama.cpp (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
2195
- : isCu
2196
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} via your custom endpoint (${customBaseUrl}). Ask away.`
2197
- : isOr
2198
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} via OpenRouter (hosted API, your OPENROUTER_API_KEY). Ask away.`
2199
- : isCp
2200
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your GitHub Copilot subscription (⚠️ experimental, ToS risk — you opted in). Ask away.`
2201
- : `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
2246
+ const readyText = reg
2247
+ ? reg.readyMessage(agentLabel)
2248
+ : isCx
2249
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Codex (ChatGPT) account. Ask away.`
2250
+ : isCg
2251
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your ChatGPT subscription (direct OAuth). Ask away.`
2252
+ : isGm
2253
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google account (Gemini Code Assist). Ask away.`
2254
+ : isAg
2255
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google AI subscription via Antigravity CLI. Note: agy turns show final answers only (no live tool progress). Ask away.`
2256
+ : isGk
2257
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Grok (xAI) account. Ask away.`
2258
+ : isOl
2259
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via Ollama (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
2260
+ : isLs
2261
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via LM Studio (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
2262
+ : isLc
2263
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via llama.cpp (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
2264
+ : isCu
2265
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} via your custom endpoint (${customBaseUrl}). Ask away.`
2266
+ : isOr
2267
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} via OpenRouter (hosted API, your OPENROUTER_API_KEY). Ask away.`
2268
+ : isCp
2269
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your GitHub Copilot subscription (⚠️ experimental, ToS risk — you opted in). Ask away.`
2270
+ : `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
2202
2271
  bridge.push({ type: "say", text: readyText }, panelTab);
2203
2272
  }
2204
2273
  bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend }, panelTab);
2205
2274
  logger.debug(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) — agent healthy, ready ack`);
2206
2275
  }
2207
2276
  else {
2208
- const degradedText = isCx
2209
- ? "⚠️ The background agent isn't responding — the Codex app-server couldn't start. Make sure Codex is installed and signed in (run `codex login`), then Disconnect → Connect to retry."
2210
- : isCg
2211
- ? "⚠️ The background agent isn't responding — ChatGPT direct OAuth couldn't start. Make sure ~/.codex/auth.json exists (run `codex login`), then Disconnect → Connect to retry."
2212
- : isGm
2213
- ? "⚠️ The background agent isn't responding — the Gemini CLI couldn't start. Make sure the Gemini CLI is installed and signed in (run `gemini` once and complete the Google sign-in), then Disconnect → Connect to retry."
2214
- : isGk
2215
- ? "⚠️ The background agent isn't responding — the Grok CLI couldn't start. Make sure Grok is installed and signed in (run `grok` once and complete the xAI sign-in), then Disconnect → Connect to retry."
2216
- : isGl
2217
- ? "⚠️ The background agent isn't responding — GLM Code API couldn't start. Set ZAI_API_KEY (Z.AI Coding Plan), then Disconnect → Connect to retry."
2218
- : isKm
2219
- ? "⚠️ The background agent isn't responding — Kimi Code couldn't start. Run Kimi Code login (~/.kimi/credentials/kimi-code.json) or set KIMI_API_KEY, then Disconnect → Connect to retry."
2220
- : isMs
2221
- ? "⚠️ The background agent isn't responding — Moonshot (Kimi K3) couldn't start. Set MOONSHOT_API_KEY from platform.kimi.ai, then Disconnect → Connect to retry."
2222
- : isOl
2223
- ? "⚠️ The background agent isn't responding — Ollama isn't reachable. Start it with `ollama serve` and pull our fine-tuned model (`ollama pull artokun/gemma4-comfyui-mcp:e4b` — gemma4 trained on the comfyui-mcp tool suite — arena-best local model; `:12b` for ~8 GB VRAM), then Disconnect → Connect to retry."
2224
- : isLs
2225
- ? `⚠️ The background agent isn't responding — LM Studio isn't reachable at ${LMSTUDIO_BASE_URL}. Open LM Studio → Developer → Start Server and load a tool-calling model (our gemma4-comfyui-mcp GGUFs from Hugging Face work great), or set COMFYUI_MCP_LMSTUDIO_HOST if it serves elsewhere — then Disconnect → Connect to retry.`
2226
- : isLc
2227
- ? `⚠️ The background agent isn't responding — llama-server isn't reachable at ${LLAMACPP_BASE_URL}. Start it with \`llama-server -m <model>.gguf -c 16384\` (our gemma4-comfyui-mcp GGUFs work great; add --jinja on older builds — required there for tool calling), or set COMFYUI_MCP_LLAMACPP_HOST — then Disconnect → Connect to retry.`
2228
- : isCu
2229
- ? `⚠️ The background agent isn't responding — your custom endpoint isn't answering at ${customBaseUrl}. Check the base URL in Settings → Custom endpoint (it must be OpenAI-compatible and include the /v1) and the API key if the server requires one — then Connect to retry.`
2230
- : isCp
2231
- ? "⚠️ The background agent isn't responding — GitHub Copilot (experimental) couldn't start. Sign in from the panel's experimental row, then Disconnect → Connect to retry."
2232
- : "⚠️ The background agent isn't responding — the Claude Agent SDK couldn't start. Make sure you're signed in (run `claude` once), then Disconnect → Connect to retry.";
2277
+ const degradedText = reg
2278
+ ? reg.degradedMessage
2279
+ : isCx
2280
+ ? "⚠️ The background agent isn't responding — the Codex app-server couldn't start. Make sure Codex is installed and signed in (run `codex login`), then Disconnect → Connect to retry."
2281
+ : isCg
2282
+ ? "⚠️ The background agent isn't responding — ChatGPT direct OAuth couldn't start. Make sure ~/.codex/auth.json exists (run `codex login`), then Disconnect → Connect to retry."
2283
+ : isGm
2284
+ ? "⚠️ The background agent isn't responding — the Gemini CLI couldn't start. Make sure the Gemini CLI is installed and signed in (run `gemini` once and complete the Google sign-in), then Disconnect → Connect to retry."
2285
+ : isAg
2286
+ ? "⚠️ The background agent isn't responding — the Antigravity CLI couldn't answer `agy models`. Install it from https://antigravity.google, run `agy` once and complete the Google Sign-In, then Disconnect → Connect to retry."
2287
+ : isGk
2288
+ ? "⚠️ The background agent isn't responding — the Grok CLI couldn't start. Make sure Grok is installed and signed in (run `grok` once and complete the xAI sign-in), then Disconnect → Connect to retry."
2289
+ : isOl
2290
+ ? "⚠️ The background agent isn't responding — Ollama isn't reachable. Start it with `ollama serve` and pull our fine-tuned model (`ollama pull artokun/gemma4-comfyui-mcp:e4b` — gemma4 trained on the comfyui-mcp tool suite — arena-best local model; `:12b` for ~8 GB VRAM), then Disconnect → Connect to retry."
2291
+ : isLs
2292
+ ? `⚠️ The background agent isn't responding — LM Studio isn't reachable at ${LMSTUDIO_BASE_URL}. Open LM Studio → Developer → Start Server and load a tool-calling model (our gemma4-comfyui-mcp GGUFs from Hugging Face work great), or set COMFYUI_MCP_LMSTUDIO_HOST if it serves elsewhere — then Disconnect → Connect to retry.`
2293
+ : isLc
2294
+ ? `⚠️ The background agent isn't responding — llama-server isn't reachable at ${LLAMACPP_BASE_URL}. Start it with \`llama-server -m <model>.gguf -c 16384\` (our gemma4-comfyui-mcp GGUFs work great; add --jinja on older builds — required there for tool calling), or set COMFYUI_MCP_LLAMACPP_HOST — then Disconnect → Connect to retry.`
2295
+ : isCu
2296
+ ? `⚠️ The background agent isn't responding — your custom endpoint isn't answering at ${customBaseUrl}. Check the base URL in Settings → Custom endpoint (it must be OpenAI-compatible and include the /v1) and the API key if the server requires one — then Connect to retry.`
2297
+ : isCp
2298
+ ? "⚠️ The background agent isn't responding — GitHub Copilot (experimental) couldn't start. Sign in from the panel's experimental row, then Disconnect → Connect to retry."
2299
+ : "⚠️ The background agent isn't responding — the Claude Agent SDK couldn't start. Make sure you're signed in (run `claude` once), then Disconnect → Connect to retry.";
2233
2300
  bridge.push({ type: "say", text: degradedText }, panelTab);
2234
2301
  bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
2235
2302
  logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) but model probe empty — degraded ack`);
@@ -2939,7 +3006,17 @@ export async function runPanelOrchestrator() {
2939
3006
  type: "say",
2940
3007
  text: "⏸ A render is running, so I've kept the GPU free for it and queued your message. I'll answer the moment it finishes.",
2941
3008
  }, event.tab_id);
2942
- bridge.push({ type: "turn", state: "idle" }, event.tab_id); // clear the working spinner
3009
+ // Clear the working spinner — the panel's turn handler only recognizes
3010
+ // "working" and "done", so the old "idle" frame never cleared it and the
3011
+ // spinner ran until the 120s safety timeout (issue #257). But ONLY when no
3012
+ // agent turn is actually in flight for this tab: a tab-wide "done" during
3013
+ // an ACTIVE earlier turn would hide THAT turn's spinner and disarm its
3014
+ // resume nudge (the idle frame was a load-bearing no-op in that case —
3015
+ // #260 review). When a turn IS active we push nothing: the live turn's own
3016
+ // turn:"done" clears the spinner at the right moment.
3017
+ if (!manager.isTurnActive(key)) {
3018
+ bridge.push({ type: "turn", state: "done" }, event.tab_id);
3019
+ }
2943
3020
  return;
2944
3021
  }
2945
3022
  manager.send(agentKeyFor(event.tab_id), outText, sendOpts);
@@ -3017,9 +3094,47 @@ export async function runPanelOrchestrator() {
3017
3094
  // `queue_status` frame at most once per second, and ONLY when the state
3018
3095
  // changed, so an idle rig costs the tabs nothing (see
3019
3096
  // services/queue-status-broadcast.ts for the frame shape + throttle contract).
3020
- const queueStatusBroadcaster = createQueueStatusBroadcaster(() => QueueMonitor.snapshot(), (frame) => void bridge.push(frame));
3021
- const queueStatusTimer = setInterval(() => queueStatusBroadcaster.tick(), 1000);
3097
+ const queueStatusBroadcaster = createQueueStatusBroadcaster(() => QueueMonitor.snapshot(), (frame) => void bridge.push(frame), () => QueueMonitor.drainCompletions());
3098
+ // Each tick first refreshes the monitor over HTTP (GET /queue + /history
3099
+ // tail): on modern ComfyUI (0.28+) the passive watchdog WS carries no
3100
+ // prompt_id and no completion events for foreign runs, so the poll is what
3101
+ // restores run attribution (#258) and catches runs shorter than the tick
3102
+ // (#259). poll() never rejects and self-guards against overlap.
3103
+ const queueStatusTimer = setInterval(() => {
3104
+ void QueueMonitor.poll().finally(() => queueStatusBroadcaster.tick());
3105
+ }, 1000);
3022
3106
  queueStatusTimer.unref?.();
3107
+ // RunPod live-status broadcast + idle auto-stop (services/runpod-watch.ts).
3108
+ // Polls the WATCHED pod (set by runpod_pod_connect / runpod_watch) every ~15s
3109
+ // and pushes a `runpod_status` frame to the panel/mobile control panels; when
3110
+ // the connected pod's ComfyUI sits idle past RUNPOD_IDLE_STOP_MINUTES (default
3111
+ // 15; 0 disables) it auto-stops the pod to save GPU cost (gpu-cli parity). No
3112
+ // pod watched → the poller is a no-op, so this costs an idle rig nothing.
3113
+ const runpodIdleStopMinutes = (() => {
3114
+ const v = Number(process.env.RUNPOD_IDLE_STOP_MINUTES);
3115
+ return Number.isFinite(v) && v >= 0 ? v : 15;
3116
+ })();
3117
+ initRunpodWatcher({
3118
+ push: (frame) => void bridge.push(frame),
3119
+ comfyuiIdle: () => {
3120
+ const s = QueueMonitor.snapshot();
3121
+ return s.connected && !s.running && s.queueDepth === 0;
3122
+ },
3123
+ // Idle auto-stop only applies to a pod we're actually rendering on: the active
3124
+ // ComfyUI target is that pod's proxy (its id appears in the URL). A pod we
3125
+ // merely watch while it boots stays local-targeted, so this is false and it is
3126
+ // never auto-stopped on the local rig's idleness.
3127
+ renderingOnPod: (podId) => !isTargetingLocal() && getComfyUIBaseUrl().includes(podId),
3128
+ idleStopMinutes: runpodIdleStopMinutes,
3129
+ });
3130
+ // Honest host indicator: whenever the ComfyUI target moves (RunPod connect,
3131
+ // pod stop → local fallback, "Local" switch), broadcast a `comfyui_target`
3132
+ // frame so every control panel truthfully shows where renders run. Seeded per
3133
+ // tab on connect (below) so a fresh tab knows the host without waiting for a
3134
+ // switch.
3135
+ onComfyuiTargetChanged((url, isLocal) => {
3136
+ void bridge.push({ type: "comfyui_target", url, is_local: isLocal });
3137
+ });
3023
3138
  // Keep the pod's stored bridge URL fresh so a ComfyUI RESTART self-heals fast.
3024
3139
  // The panel's advertised wss:// URL/token lives in the pod ComfyUI process's
3025
3140
  // MEMORY (the panel __init__'s advertise store). A restart — which the agent
@@ -3111,6 +3226,11 @@ export async function runPanelOrchestrator() {
3111
3226
  // opt-out: COMFYUI_MCP_AUTORESTART=0).
3112
3227
  selfRestarter = new SelfRestarter({
3113
3228
  allIdle: () => manager.allIdle() &&
3229
+ // Failed-start held mail (issue #256): teardown erases it, so a restart
3230
+ // while it's parked would silently drop the messages awaiting re-delivery.
3231
+ // (allIdle() already covers this; kept explicit alongside heldDuringGen so
3232
+ // the gate reads as the full "nothing queued or held" contract.)
3233
+ !manager.hasHeldMail() &&
3114
3234
  ![...heldDuringGen.values()].some((msgs) => msgs.length > 0) &&
3115
3235
  !QueueMonitor.isBusy(),
3116
3236
  announce: (text) => void bridge.push({ type: "say", text }),