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.
- package/README.md +8 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -1
- package/dist/orchestrator/agent-backend.js +24 -10
- package/dist/orchestrator/agent-backend.js.map +1 -1
- package/dist/orchestrator/antigravity-backend.js +575 -0
- package/dist/orchestrator/antigravity-backend.js.map +1 -0
- package/dist/orchestrator/backend-readiness.js +23 -14
- package/dist/orchestrator/backend-readiness.js.map +1 -1
- package/dist/orchestrator/copilot-backend.js +3 -2
- package/dist/orchestrator/copilot-backend.js.map +1 -1
- package/dist/orchestrator/index.js +267 -147
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +138 -18
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/orchestrator/panel-console-http.js +4 -3
- package/dist/orchestrator/panel-console-http.js.map +1 -1
- package/dist/orchestrator/start-failure-notice.js +63 -0
- package/dist/orchestrator/start-failure-notice.js.map +1 -0
- package/dist/services/code-provider-auth.js +49 -14
- package/dist/services/code-provider-auth.js.map +1 -1
- package/dist/services/job-history.js +50 -0
- package/dist/services/job-history.js.map +1 -1
- package/dist/services/job-watcher.js +2 -0
- package/dist/services/job-watcher.js.map +1 -1
- package/dist/services/openai-provider-registry.js +87 -0
- package/dist/services/openai-provider-registry.js.map +1 -0
- package/dist/services/panel-secrets.js +167 -59
- package/dist/services/panel-secrets.js.map +1 -1
- package/dist/services/queue-manager.js +2 -1
- package/dist/services/queue-manager.js.map +1 -1
- package/dist/services/queue-monitor.js +241 -7
- package/dist/services/queue-monitor.js.map +1 -1
- package/dist/services/queue-monitor.poll.test.js +267 -0
- package/dist/services/queue-monitor.poll.test.js.map +1 -0
- package/dist/services/queue-status-broadcast.js +26 -9
- package/dist/services/queue-status-broadcast.js.map +1 -1
- package/dist/services/queue-status-broadcast.test.js +49 -0
- package/dist/services/queue-status-broadcast.test.js.map +1 -1
- package/dist/services/runpod-client.js +190 -0
- package/dist/services/runpod-client.js.map +1 -0
- package/dist/services/runpod-watch.js +174 -0
- package/dist/services/runpod-watch.js.map +1 -0
- package/dist/tools/diagnostics.js +54 -27
- package/dist/tools/diagnostics.js.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/queue-management.js +1 -1
- package/dist/tools/queue-management.js.map +1 -1
- package/dist/tools/runpod.js +285 -0
- package/dist/tools/runpod.js.map +1 -0
- package/package.json +1 -1
- package/plugin/skills/ltx-director/SKILL.md +174 -0
- package/dist/orchestrator/glm-backend.js +0 -20
- package/dist/orchestrator/glm-backend.js.map +0 -1
- package/dist/orchestrator/moonshot-backend.js +0 -22
- 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 {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
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
|
|
174
|
-
"
|
|
175
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
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 === "
|
|
1444
|
-
? new
|
|
1445
|
-
: backend === "
|
|
1446
|
-
? new
|
|
1447
|
-
: backend === "
|
|
1448
|
-
? new
|
|
1449
|
-
|
|
1450
|
-
?
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
|
1567
|
-
//
|
|
1568
|
-
// handshake — exactly the wedge. Exit
|
|
1569
|
-
// → reclaim + sticky-reconnect
|
|
1570
|
-
// leaving the user staring at the
|
|
1571
|
-
//
|
|
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
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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
|
-
|
|
2077
|
-
const
|
|
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 =
|
|
2120
|
-
? (
|
|
2121
|
-
:
|
|
2122
|
-
? (
|
|
2123
|
-
:
|
|
2124
|
-
? (
|
|
2125
|
-
:
|
|
2126
|
-
? (
|
|
2127
|
-
:
|
|
2128
|
-
? (
|
|
2129
|
-
:
|
|
2130
|
-
? (
|
|
2131
|
-
:
|
|
2132
|
-
? (
|
|
2133
|
-
:
|
|
2134
|
-
? (
|
|
2135
|
-
:
|
|
2136
|
-
? (
|
|
2137
|
-
:
|
|
2138
|
-
? (
|
|
2139
|
-
:
|
|
2140
|
-
? (
|
|
2141
|
-
:
|
|
2142
|
-
? (
|
|
2143
|
-
:
|
|
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 =
|
|
2176
|
-
?
|
|
2177
|
-
:
|
|
2178
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your
|
|
2179
|
-
:
|
|
2180
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your
|
|
2181
|
-
:
|
|
2182
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your
|
|
2183
|
-
:
|
|
2184
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your
|
|
2185
|
-
:
|
|
2186
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your
|
|
2187
|
-
:
|
|
2188
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel}
|
|
2189
|
-
:
|
|
2190
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via
|
|
2191
|
-
:
|
|
2192
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via
|
|
2193
|
-
:
|
|
2194
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel}
|
|
2195
|
-
:
|
|
2196
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} via
|
|
2197
|
-
:
|
|
2198
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel}
|
|
2199
|
-
:
|
|
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 =
|
|
2209
|
-
?
|
|
2210
|
-
:
|
|
2211
|
-
? "⚠️ The background agent isn't responding —
|
|
2212
|
-
:
|
|
2213
|
-
? "⚠️ The background agent isn't responding —
|
|
2214
|
-
:
|
|
2215
|
-
? "⚠️ The background agent isn't responding — the
|
|
2216
|
-
:
|
|
2217
|
-
? "⚠️ The background agent isn't responding —
|
|
2218
|
-
:
|
|
2219
|
-
? "⚠️ The background agent isn't responding —
|
|
2220
|
-
:
|
|
2221
|
-
? "⚠️ The background agent isn't responding —
|
|
2222
|
-
:
|
|
2223
|
-
?
|
|
2224
|
-
:
|
|
2225
|
-
? `⚠️ The background agent isn't responding —
|
|
2226
|
-
:
|
|
2227
|
-
? `⚠️ The background agent isn't responding —
|
|
2228
|
-
:
|
|
2229
|
-
?
|
|
2230
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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 }),
|