comfyui-mcp 0.22.0 → 0.23.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 +24 -0
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/codex-backend.js +32 -4
- package/dist/orchestrator/codex-backend.js.map +1 -1
- package/dist/orchestrator/index.js +315 -154
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +49 -14
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/services/env-capabilities.js +95 -1
- package/dist/services/env-capabilities.js.map +1 -1
- package/dist/services/image-management.js +100 -1
- package/dist/services/image-management.js.map +1 -1
- package/dist/services/manifest.js +71 -14
- package/dist/services/manifest.js.map +1 -1
- package/dist/services/model-resolver.js +143 -1
- package/dist/services/model-resolver.js.map +1 -1
- package/dist/services/node-management.js +46 -0
- package/dist/services/node-management.js.map +1 -1
- package/dist/tools/image-management.js +8 -4
- package/dist/tools/image-management.js.map +1 -1
- package/dist/tools/manifest.js +1 -1
- package/dist/tools/manifest.js.map +1 -1
- package/dist/tools/model-extras.js +27 -8
- package/dist/tools/model-extras.js.map +1 -1
- package/dist/tools/model-management.js +1 -1
- package/dist/tools/model-management.js.map +1 -1
- package/dist/tools/node-management.js +21 -0
- package/dist/tools/node-management.js.map +1 -1
- package/dist/transport/cli.js +39 -2
- package/dist/transport/cli.js.map +1 -1
- package/package.json +1 -1
- package/packs/wan-multitalk/install-runpod.sh +44 -0
- package/packs/wan-multitalk/install-windows.bat +49 -0
- package/packs/wan-multitalk/manifest.yaml +50 -0
- package/packs/wan-multitalk/pack.yaml +28 -0
- package/packs/wan-multitalk/workflow.json +1432 -0
- package/plugin/skills/triton-sageattention/SKILL.md +15 -0
- package/plugin/skills/troubleshooting/SKILL.md +56 -0
- package/plugin/skills/wan-multitalk/SKILL.md +97 -0
|
@@ -56,6 +56,8 @@ RESOLVING A TANGLED / TOGGLE-HEAVY WORKFLOW (Get/Set buses + rgthree-bypassed pi
|
|
|
56
56
|
|
|
57
57
|
DOWNLOADING MODELS — use the download_model tool, NOT a raw shell download. When a workflow needs model weights you don't have (checkpoints, LoRAs, VAEs, text encoders, etc.), download them with the comfyui MCP download_model tool (or download_civitai_model for CivitAI): it streams the file into the correct ComfyUI models/ subfolder AND surfaces live progress in the panel's download tray so the user can watch it. Pass target_subfolder to land the file exactly where it belongs (e.g. 'loras', 'checkpoints', 'vae', 'text_encoders', or a nested path like 'loras/<subdir>'). Do NOT shell out to curl/wget/aria2 for model files — a raw shell download has no progress in the panel and can drop the file in the wrong place. Reserve the shell for things download_model can't do.
|
|
58
58
|
|
|
59
|
+
HARDWARE & RUNTIME STATS — use the MCP tools, NOT the shell. For GPU / VRAM / CPU / RAM, CUDA/torch/python versions, and ComfyUI runtime stats, call the comfyui MCP get_system_stats (raw /system_stats) or get_environment (a summarized snapshot) — they read the CONNECTED ComfyUI's /system_stats and work for LOCAL and REMOTE targets alike. Do NOT shell out (nvidia-smi, PowerShell, wmic, python) for hardware info: the managed shell is sandboxed/read-only and rejects multi-line scripts, so those probes fail and only reach the orchestrator host anyway, not a remote ComfyUI. The startup ENVIRONMENT line already summarizes the machine; when you need current or more detail, get_system_stats / get_environment are the source of truth.
|
|
60
|
+
|
|
59
61
|
LOCAL-GPU (FREE) vs API NODES (PAID CREDITS) — and ASK before spending. ComfyUI workflows are either LOCAL — they run on the user's OWN GPU, which is free — or they use API NODES (hosted/partner services) that consume the user's PAID api credits. The bundled installer packs (list_packs) are ALL local/free; ComfyUI's official templates and any ad-hoc or generated workflow MAY use API nodes. BEFORE you build OR load any workflow that uses API nodes, you MUST ASK the user whether to use the free local GPU or paid api credits, and NEVER silently spend credits. To tell the difference reliably, call check_workflow_runtime(pack:<name> or graph:<json>) — it returns { runtime: 'local'|'api'|'mixed'|'unknown', usesApiNodes, apiNodes[] } by scanning the graph's nodes against the server's API-node set; treat 'api'/'mixed' (usesApiNodes:true) AND 'unknown' (unclassifiable nodes that could be paid) as POSSIBLY PAID and stop to ask — only 'local' is confirmed free. DEFAULT TO / PREFER the local pack unless the user explicitly opts into API nodes. Packs are always safe to load without asking; check ad-hoc/template workflows first.
|
|
60
62
|
|
|
61
63
|
You also have the comfyui MCP tools to generate images, video, and audio and to inspect, download models for, and manage their ComfyUI instance. Use them to actually do what's asked, then tell them what you did and name or link any output. If a request is ambiguous, make a sensible choice and say what you chose rather than stalling.
|
|
@@ -342,11 +344,28 @@ export async function runPanelOrchestrator() {
|
|
|
342
344
|
// mode — so it generates against the live ComfyUI over COMFYUI_URL and never
|
|
343
345
|
// tries to bind the bridge port we own here.
|
|
344
346
|
const mcpEntry = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
347
|
+
// Mutable: the panel sends the ComfyUI URL it was SERVED FROM (window.location)
|
|
348
|
+
// in `hello`, and the orchestrator retargets to it (applyComfyuiUrl) — so
|
|
349
|
+
// `--panel-orchestrator` boots on the localhost default and auto-points at
|
|
350
|
+
// whatever ComfyUI (local or a RunPod proxy) the browser is actually on. No
|
|
351
|
+
// `connect <url>` needed.
|
|
352
|
+
let comfyuiUrl = process.env.COMFYUI_URL ?? "http://127.0.0.1:8188";
|
|
353
|
+
// ComfyUI install path — when set AND the target is loopback, the spawned agent's
|
|
354
|
+
// MCP runs in LOCAL mode (download_model / apply_manifest / installer-pack /
|
|
355
|
+
// model-scan tools). A REMOTE target (non-loopback) forces remote-only, so we
|
|
356
|
+
// drop the path. `envComfyuiPath` is the orchestrator's own env value; the live
|
|
357
|
+
// `comfyuiPath` is derived from it + the current target.
|
|
358
|
+
const envComfyuiPath = process.env.COMFYUI_PATH;
|
|
359
|
+
const isLoopbackUrl = (u) => {
|
|
360
|
+
try {
|
|
361
|
+
const h = new URL(u).hostname.toLowerCase();
|
|
362
|
+
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "0.0.0.0" || h === "";
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
let comfyuiPath = isLoopbackUrl(comfyuiUrl) ? envComfyuiPath : undefined;
|
|
350
369
|
const model = process.env.COMFYUI_MCP_PANEL_MODEL ?? "claude-opus-4-8";
|
|
351
370
|
const envEffort = process.env.COMFYUI_MCP_PANEL_EFFORT;
|
|
352
371
|
const effort = isEffort(envEffort) ? envEffort : undefined;
|
|
@@ -409,12 +428,31 @@ export async function runPanelOrchestrator() {
|
|
|
409
428
|
// COMFYUI_MCP_GEMINI_MODEL (default gemini-2.5-pro). The model is applied at spawn
|
|
410
429
|
// via the CLI `--model` flag (ACP exposes no per-session model setter).
|
|
411
430
|
const geminiModel = process.env.COMFYUI_MCP_GEMINI_MODEL ?? GEMINI_DEFAULT_MODEL;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
431
|
+
// ── Per-tab backend (single-port multi-provider) ──────────────────────────
|
|
432
|
+
// ONE orchestrator on ONE bridge port serves ALL providers; the panel picks a
|
|
433
|
+
// provider per tab via the `hello`/`set_backend` handshake, instead of the node
|
|
434
|
+
// spawning one process per provider on its own port (9180/9181/9182). Internally
|
|
435
|
+
// each (panel tab, backend) pair is one agent addressed by a composite key
|
|
436
|
+
// `tabId::backend`, so switching provider starts a FRESH session for that
|
|
437
|
+
// provider (the panel replays the transcript to seed it) while a same-provider
|
|
438
|
+
// reconnect RESUMES. `backendId`/`codexModel`/`geminiModel` above are the
|
|
439
|
+
// DEFAULT + per-provider model config; the process is no longer pinned to one.
|
|
440
|
+
const KNOWN_BACKENDS = new Set(["claude", "codex", "gemini"]);
|
|
441
|
+
const defaultBackend = KNOWN_BACKENDS.has(backendId) ? backendId : "claude";
|
|
442
|
+
const AGENT_KEY_SEP = "::";
|
|
443
|
+
const tabBackends = new Map(); // panel tabId -> selected backend
|
|
444
|
+
const backendForTab = (panelTabId) => tabBackends.get(panelTabId) ?? defaultBackend;
|
|
445
|
+
const agentKeyFor = (panelTabId) => panelTabId + AGENT_KEY_SEP + backendForTab(panelTabId);
|
|
446
|
+
// A panel tab id never contains "::"; backend names never do — so split on the
|
|
447
|
+
// LAST separator to recover each half from a composite key.
|
|
448
|
+
const panelTabOf = (key) => {
|
|
449
|
+
const i = key.lastIndexOf(AGENT_KEY_SEP);
|
|
450
|
+
return i >= 0 ? key.slice(0, i) : key;
|
|
451
|
+
};
|
|
452
|
+
const backendOf = (key) => {
|
|
453
|
+
const i = key.lastIndexOf(AGENT_KEY_SEP);
|
|
454
|
+
return i >= 0 ? key.slice(i + AGENT_KEY_SEP.length) : defaultBackend;
|
|
455
|
+
};
|
|
418
456
|
// ---- live ENVIRONMENT-CAPABILITIES block ----
|
|
419
457
|
// Gather the machine's facts ONCE at startup (CACHED) — OS/CPU/RAM from node,
|
|
420
458
|
// GPU/VRAM/CUDA/torch/python/ComfyUI from /system_stats, Triton/SageAttention by
|
|
@@ -462,7 +500,9 @@ export async function runPanelOrchestrator() {
|
|
|
462
500
|
// A panel-saved tool secret (CIVITAI_API_TOKEN, HF_TOKEN, …) is layered on top
|
|
463
501
|
// by buildComfyuiMcpEnv() at SPAWN time, so the same headless tool surface — and
|
|
464
502
|
// the same secrets — reach either provider.
|
|
465
|
-
|
|
503
|
+
// A FUNCTION (not a frozen object) so it always reflects the CURRENT retargeted
|
|
504
|
+
// comfyuiUrl/comfyuiPath — makeHttpBackendMcpServers calls it per (re)spawn.
|
|
505
|
+
const comfyuiBaseEnv = () => ({
|
|
466
506
|
COMFYUI_URL: comfyuiUrl,
|
|
467
507
|
COMFYUI_MCP_PROGRESS_DIR: progressDir,
|
|
468
508
|
...(comfyuiPath ? { COMFYUI_PATH: comfyuiPath } : {}),
|
|
@@ -472,19 +512,23 @@ export async function runPanelOrchestrator() {
|
|
|
472
512
|
...(process.env.HF_TOKEN ? { HF_TOKEN: process.env.HF_TOKEN } : {}),
|
|
473
513
|
// Test-only tool-call trace (knowledge-parity smoke). No-op unless set.
|
|
474
514
|
...(process.env.COMFYUI_MCP_TOOL_TRACE ? { COMFYUI_MCP_TOOL_TRACE: process.env.COMFYUI_MCP_TOOL_TRACE } : {}),
|
|
475
|
-
};
|
|
515
|
+
});
|
|
476
516
|
// The orchestrator-hosted loopback HTTP MCP for panel_* tools. Started for the
|
|
477
517
|
// non-Claude backends (Codex + Gemini), which can't host an in-process SDK MCP
|
|
478
518
|
// server the way Claude does. Port: COMFYUI_MCP_PANEL_MCP_PORT, default
|
|
479
519
|
// bridgePort+1 (loopback only).
|
|
520
|
+
// Start the loopback HTTP panel-MCP ALWAYS: with single-port multi-provider any
|
|
521
|
+
// tab may pick codex/gemini at runtime, and those backends drive the canvas
|
|
522
|
+
// through this server (Claude tabs use the in-process SDK server instead). The
|
|
523
|
+
// per-tab session routing (`urlFor(panelTabId)`) already isolates tabs.
|
|
480
524
|
let panelMcpHttp = null;
|
|
481
|
-
|
|
525
|
+
{
|
|
482
526
|
const panelMcpPort = Number(process.env.COMFYUI_MCP_PANEL_MCP_PORT) || bridgePort + 1;
|
|
483
527
|
try {
|
|
484
528
|
panelMcpHttp = await startPanelMcpHttpServer(bridge, panelMcpPort);
|
|
485
529
|
}
|
|
486
530
|
catch (err) {
|
|
487
|
-
logger.error(`[panel-orchestrator] could not start the panel HTTP MCP on :${panelMcpPort} —
|
|
531
|
+
logger.error(`[panel-orchestrator] could not start the panel HTTP MCP on :${panelMcpPort} — codex/gemini tabs will lack live-graph tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
488
532
|
}
|
|
489
533
|
}
|
|
490
534
|
// Shared MCP server config for BOTH the Codex and Gemini backends — they take an
|
|
@@ -498,52 +542,61 @@ export async function runPanelOrchestrator() {
|
|
|
498
542
|
args: [mcpEntry], // dist/index.js
|
|
499
543
|
// Merge persisted tool secrets at SPAWN time so a respawn picks up a
|
|
500
544
|
// just-saved CIVITAI_API_TOKEN / HF_TOKEN without a process restart.
|
|
501
|
-
env: buildComfyuiMcpEnv(comfyuiBaseEnv),
|
|
545
|
+
env: buildComfyuiMcpEnv(comfyuiBaseEnv()),
|
|
502
546
|
},
|
|
503
547
|
// Live-graph panel_* tools for THIS tab over the loopback HTTP MCP.
|
|
504
548
|
...(panelMcpHttp
|
|
505
549
|
? { panel: { transport: "http", url: panelMcpHttp.urlFor(tabId) } }
|
|
506
550
|
: {}),
|
|
507
551
|
});
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
552
|
+
// Build the provider backend for a composite agent key `panelTabId::backend`.
|
|
553
|
+
// Claude → undefined (PanelAgent uses its built-in in-process SDK backend);
|
|
554
|
+
// codex/gemini → their CLI-driven backend, wired to the panel_* tools over the
|
|
555
|
+
// loopback HTTP MCP for THIS panel tab's canvas (comfyuiUrl gives vision parity).
|
|
556
|
+
const makeBackend = (key) => {
|
|
557
|
+
const backend = backendOf(key);
|
|
558
|
+
const panelTabId = panelTabOf(key);
|
|
559
|
+
if (backend === "codex") {
|
|
560
|
+
return new CodexBackend({
|
|
561
|
+
cwd: comfyuiPath ?? process.cwd(),
|
|
562
|
+
model: codexModel,
|
|
563
|
+
systemAppend: panelSystemAppend,
|
|
564
|
+
comfyuiUrl,
|
|
565
|
+
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (backend === "gemini") {
|
|
569
|
+
return new GeminiBackend({
|
|
520
570
|
cwd: comfyuiPath ?? process.cwd(),
|
|
521
571
|
model: geminiModel,
|
|
522
572
|
systemAppend: panelSystemAppend,
|
|
523
|
-
// Base ComfyUI URL so the backend can fetch image bytes from /view and
|
|
524
|
-
// deliver them inline as base64 image ContentBlocks (vision parity).
|
|
525
573
|
comfyuiUrl,
|
|
526
|
-
mcpServers: makeHttpBackendMcpServers(
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
574
|
+
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return undefined; // claude → built-in ClaudeBackend
|
|
578
|
+
};
|
|
579
|
+
logger.info(`[panel-orchestrator] single-port multi-provider: default backend=${defaultBackend}; ` +
|
|
580
|
+
`codex/gemini panel_* live-graph tools via loopback HTTP MCP${panelMcpHttp ? ` on :${panelMcpHttp.port}` : " UNAVAILABLE"} + headless comfyui MCP`);
|
|
581
|
+
// Readiness/model probing routes through the SELECTED backend PER TAB — a
|
|
582
|
+
// codex/gemini tab's "ready" must NOT depend on Claude SDK/login health. Claude
|
|
583
|
+
// uses fetchSupportedModels(); codex/gemini spin up a throwaway probe backend
|
|
584
|
+
// (which also proves the CLI can launch). Cached per backend so repeated hellos
|
|
585
|
+
// don't re-probe.
|
|
586
|
+
const probeBackends = new Map();
|
|
587
|
+
const getProbeBackend = (backend) => {
|
|
588
|
+
if (backend === "claude")
|
|
589
|
+
return null; // claude uses the SDK probe below
|
|
590
|
+
let pb = probeBackends.get(backend);
|
|
591
|
+
if (!pb) {
|
|
592
|
+
pb =
|
|
593
|
+
backend === "codex"
|
|
594
|
+
? new CodexBackend({ cwd: comfyuiPath ?? process.cwd(), model: codexModel })
|
|
595
|
+
: new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
|
|
596
|
+
probeBackends.set(backend, pb);
|
|
597
|
+
}
|
|
598
|
+
return pb;
|
|
599
|
+
};
|
|
547
600
|
// Durable per-tab session ids (keyed by our bridge port), so a tab's agent
|
|
548
601
|
// resumes its conversation even after the orchestrator PROCESS is killed and
|
|
549
602
|
// respawned (a wedge auto-restart) — not just a soft reload.
|
|
@@ -577,42 +630,46 @@ export async function runPanelOrchestrator() {
|
|
|
577
630
|
comfyuiUrl, // for fetching image bytes to inline into agent turns
|
|
578
631
|
systemAppend: panelSystemAppend,
|
|
579
632
|
pluginPath: pluginAvailable ? pluginPath : undefined,
|
|
580
|
-
//
|
|
581
|
-
|
|
633
|
+
// In-process live-graph MCP for CLAUDE keys only (codex/gemini drive the
|
|
634
|
+
// canvas through the loopback HTTP MCP instead). Bound to the PANEL tab so
|
|
635
|
+
// panel_* tools reach the user's canvas regardless of the composite key.
|
|
636
|
+
makePanelServer: (key) => backendOf(key) === "claude" ? createPanelMcpServer(bridge, panelTabOf(key)) : undefined,
|
|
582
637
|
mcpServers: buildMcpServers(),
|
|
583
|
-
|
|
638
|
+
// NOTE: manager callbacks fire with the composite agent key `tabId::backend`;
|
|
639
|
+
// panelTabOf() recovers the PANEL tab so every push reaches the right socket.
|
|
640
|
+
onSay: (key, text, meta) => {
|
|
584
641
|
// `id` lets the panel reconcile this committed message with its live
|
|
585
642
|
// streaming preview (same id) instead of rendering a duplicate bubble.
|
|
586
|
-
bridge.push({ type: "say", text, id: meta?.id, streamed: meta?.streamed },
|
|
643
|
+
bridge.push({ type: "say", text, id: meta?.id, streamed: meta?.streamed }, panelTabOf(key));
|
|
587
644
|
},
|
|
588
645
|
// Live streaming deltas → the panel's think-window + streaming reply bubble.
|
|
589
|
-
onStream: (
|
|
590
|
-
bridge.push({ type: "stream", phase: ev.phase, id: ev.id, delta: ev.delta },
|
|
646
|
+
onStream: (key, ev) => {
|
|
647
|
+
bridge.push({ type: "stream", phase: ev.phase, id: ev.id, delta: ev.delta }, panelTabOf(key));
|
|
591
648
|
},
|
|
592
649
|
// Per-response usage → the panel's context/usage meter (updates live).
|
|
593
|
-
onStatus: pushStatus,
|
|
650
|
+
onStatus: (key, status) => pushStatus(panelTabOf(key), status),
|
|
594
651
|
// Report the SDK session id so the panel can persist it and resume on reload.
|
|
595
|
-
onSession: (
|
|
596
|
-
bridge.push({ type: "session", session_id: sessionId },
|
|
652
|
+
onSession: (key, sessionId) => {
|
|
653
|
+
bridge.push({ type: "session", session_id: sessionId }, panelTabOf(key));
|
|
597
654
|
},
|
|
598
655
|
// Per-turn rewind anchor (assistant UUID) → the panel stores it so a later
|
|
599
656
|
// "rewind conversation to here" can fork the session at that point.
|
|
600
|
-
onTurnAnchor: (
|
|
601
|
-
bridge.push({ type: "turn_anchor", uuid },
|
|
657
|
+
onTurnAnchor: (key, uuid) => {
|
|
658
|
+
bridge.push({ type: "turn_anchor", uuid }, panelTabOf(key));
|
|
602
659
|
},
|
|
603
660
|
// Turn lifecycle → the panel's "working" indicator (stays up through silent
|
|
604
661
|
// tool work; clears on done).
|
|
605
|
-
onTurn: (
|
|
606
|
-
bridge.push({ type: "turn", state },
|
|
662
|
+
onTurn: (key, state) => {
|
|
663
|
+
bridge.push({ type: "turn", state }, panelTabOf(key));
|
|
607
664
|
},
|
|
608
665
|
// Live extended-thinking token count → "thinking… (N)" indicator.
|
|
609
|
-
onThinking: (
|
|
610
|
-
bridge.push({ type: "thinking", tokens },
|
|
666
|
+
onThinking: (key, tokens) => {
|
|
667
|
+
bridge.push({ type: "thinking", tokens }, panelTabOf(key));
|
|
611
668
|
},
|
|
612
669
|
// The agent dequeued a message (the true "read" moment) → flip that bubble
|
|
613
670
|
// from queued/muted to read.
|
|
614
|
-
onSeen: (
|
|
615
|
-
bridge.push({ type: "ack", ok: true, kind: "seen", mid },
|
|
671
|
+
onSeen: (key, mid) => {
|
|
672
|
+
bridge.push({ type: "ack", ok: true, kind: "seen", mid }, panelTabOf(key));
|
|
616
673
|
},
|
|
617
674
|
// ROOT-CAUSE self-exit (the "bridge open but no panel agent responded" wedge):
|
|
618
675
|
// a tab's agent died fatally (couldn't start, or its bounded self-restart gave
|
|
@@ -629,6 +686,52 @@ export async function runPanelOrchestrator() {
|
|
|
629
686
|
// Let refreshEnvCapabilities() feed a freshly-gathered env block into agents
|
|
630
687
|
// spawned after a ComfyUI restart/reconnect.
|
|
631
688
|
liveManager = manager;
|
|
689
|
+
// Retarget the live ComfyUI from the panel's `hello.comfyui_url` (the URL the
|
|
690
|
+
// browser was SERVED FROM — window.location). This is what lets a bare
|
|
691
|
+
// `--panel-orchestrator` (booted on the localhost default) auto-point at whatever
|
|
692
|
+
// ComfyUI the user actually has open — local OR a RunPod proxy — with no
|
|
693
|
+
// `connect <url>`. Loopback → LOCAL mode (keep COMFYUI_PATH); non-loopback →
|
|
694
|
+
// REMOTE mode (drop the path). No-op if unchanged. Returns true if it retargeted.
|
|
695
|
+
const applyComfyuiUrl = (rawUrl) => {
|
|
696
|
+
if (typeof rawUrl !== "string")
|
|
697
|
+
return false;
|
|
698
|
+
const next = rawUrl.trim().replace(/\/+$/, "");
|
|
699
|
+
if (!next)
|
|
700
|
+
return false;
|
|
701
|
+
let host;
|
|
702
|
+
try {
|
|
703
|
+
const parsed = new URL(next);
|
|
704
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
705
|
+
return false;
|
|
706
|
+
host = parsed.hostname;
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return false; // not a valid URL — ignore (keep current target)
|
|
710
|
+
}
|
|
711
|
+
if (!host || next === comfyuiUrl)
|
|
712
|
+
return false;
|
|
713
|
+
const prev = comfyuiUrl;
|
|
714
|
+
comfyuiUrl = next;
|
|
715
|
+
comfyuiPath = isLoopbackUrl(next) ? envComfyuiPath : undefined;
|
|
716
|
+
// Point every provider at the new target: Claude via its rebuilt MCP env, the
|
|
717
|
+
// manager's image-fetch URL, then respawn active agents so the live comfyui MCP
|
|
718
|
+
// subprocess is recreated with the new COMFYUI_URL (no-op if none are running —
|
|
719
|
+
// the next spawn picks it up from the now-updated closures).
|
|
720
|
+
manager.setMcpServers(buildMcpServers());
|
|
721
|
+
manager.setComfyuiUrl(comfyuiUrl);
|
|
722
|
+
manager.restartAllForMcpEnv();
|
|
723
|
+
// Re-point the render watchdog and re-probe the env (remote vs local differs).
|
|
724
|
+
try {
|
|
725
|
+
QueueMonitor.stop();
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
/* best-effort */
|
|
729
|
+
}
|
|
730
|
+
QueueMonitor.start(comfyuiUrl);
|
|
731
|
+
void refreshEnvCapabilities();
|
|
732
|
+
logger.info(`[panel-orchestrator] retargeted ComfyUI ${prev} → ${comfyuiUrl} (${isLoopbackUrl(next) ? "local" : "remote"} mode) from panel hello`);
|
|
733
|
+
return true;
|
|
734
|
+
};
|
|
632
735
|
// Tool secrets → comfyui MCP env: when the user saves a token via
|
|
633
736
|
// panel_request_secret (e.g. CIVITAI_API_TOKEN for download_civitai_model), the
|
|
634
737
|
// secret store persists it and fires this. We rebuild the comfyui server's spawn
|
|
@@ -650,52 +753,59 @@ export async function runPanelOrchestrator() {
|
|
|
650
753
|
// works on the subscription lane) and cached. Pushed to each tab so the
|
|
651
754
|
// panel's model/effort picker reflects what's actually available, with each
|
|
652
755
|
// model's supported effort levels, instead of a hardcoded list.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const probe =
|
|
666
|
-
? Promise.resolve(
|
|
667
|
-
.then(() =>
|
|
668
|
-
.then((list) =>
|
|
669
|
-
// Carry the effort metadata through to the panel's ModelInfo — without
|
|
670
|
-
// this the supportsEffort/supportedEffortLevels the Codex backend
|
|
671
|
-
// advertises get dropped here and the panel hides the effort dropdown.
|
|
672
|
-
list.map((m) => ({
|
|
756
|
+
// Model list PER BACKEND — probed lazily and cached; an empty/failed probe is
|
|
757
|
+
// NOT cached so the next hello retries. Claude uses fetchSupportedModels() (the
|
|
758
|
+
// only path that works on the subscription lane); codex/gemini enumerate via a
|
|
759
|
+
// throwaway probe backend, which also proves the CLI can launch (= readiness).
|
|
760
|
+
// (Gemini's probe proves the CLI + ACP handshake but not Google sign-in, which
|
|
761
|
+
// ACP only reports at session/new — so its "ready" is provisional; a signed-out
|
|
762
|
+
// CLI surfaces a clear one-shot error on the first turn.)
|
|
763
|
+
const modelsByBackend = new Map();
|
|
764
|
+
function ensureModels(backend) {
|
|
765
|
+
let p = modelsByBackend.get(backend);
|
|
766
|
+
if (!p) {
|
|
767
|
+
const pb = getProbeBackend(backend);
|
|
768
|
+
const probe = pb
|
|
769
|
+
? Promise.resolve(pb.prepare?.())
|
|
770
|
+
.then(() => pb.listModels())
|
|
771
|
+
.then((list) => list.map((m) => ({
|
|
673
772
|
value: m.id,
|
|
674
773
|
displayName: m.label ?? m.id,
|
|
675
774
|
...(m.supportsEffort != null ? { supportsEffort: m.supportsEffort } : {}),
|
|
676
775
|
...(m.supportedEffortLevels ? { supportedEffortLevels: m.supportedEffortLevels } : {}),
|
|
677
776
|
})))
|
|
678
777
|
.catch((err) => {
|
|
679
|
-
logger.warn(`[panel-orchestrator]
|
|
778
|
+
logger.warn(`[panel-orchestrator] ${backend} model probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
680
779
|
return [];
|
|
681
780
|
})
|
|
682
781
|
: fetchSupportedModels(model);
|
|
683
|
-
|
|
684
|
-
// Don't cache an empty/failed probe forever — let the next hello retry.
|
|
782
|
+
p = probe.then((list) => {
|
|
685
783
|
if (!list.length)
|
|
686
|
-
|
|
784
|
+
modelsByBackend.delete(backend); // don't cache a failure
|
|
687
785
|
return list;
|
|
688
786
|
});
|
|
787
|
+
modelsByBackend.set(backend, p);
|
|
689
788
|
}
|
|
690
|
-
return
|
|
789
|
+
return p;
|
|
790
|
+
}
|
|
791
|
+
// The model to highlight as "current" for a backend: the panel's configured
|
|
792
|
+
// model for claude; the env override (or account default = the list's own
|
|
793
|
+
// current) for codex/gemini.
|
|
794
|
+
function currentModelFor(backend) {
|
|
795
|
+
if (backend === "codex")
|
|
796
|
+
return codexModel;
|
|
797
|
+
if (backend === "gemini")
|
|
798
|
+
return geminiModel;
|
|
799
|
+
return model;
|
|
691
800
|
}
|
|
692
|
-
function pushModels(
|
|
693
|
-
|
|
801
|
+
function pushModels(panelTabId) {
|
|
802
|
+
const backend = backendForTab(panelTabId);
|
|
803
|
+
void ensureModels(backend)
|
|
694
804
|
.then((models) => {
|
|
695
805
|
if (models.length) {
|
|
696
|
-
// `backend` rides on the models frame so the panel's
|
|
697
|
-
//
|
|
698
|
-
bridge.push({ type: "models", models, current:
|
|
806
|
+
// `backend` rides on the models frame so the panel's picker reflects the
|
|
807
|
+
// provider THIS tab selected (single-port multi-provider).
|
|
808
|
+
bridge.push({ type: "models", models, current: currentModelFor(backend), backend }, panelTabId);
|
|
699
809
|
}
|
|
700
810
|
})
|
|
701
811
|
.catch(() => {
|
|
@@ -720,10 +830,8 @@ export async function runPanelOrchestrator() {
|
|
|
720
830
|
// the built-ins that make sense inside the ComfyUI panel chat.
|
|
721
831
|
const PANEL_SLASH_ALLOWLIST = new Set(["compact", "context", "usage", "loop", "goal", "clear"]);
|
|
722
832
|
function pushCommands(tabId) {
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
if (isHttpPanelBackend)
|
|
726
|
-
return;
|
|
833
|
+
// Claude-only: SDK slash-commands don't exist for codex/gemini. Callers already
|
|
834
|
+
// gate this to claude tabs (single-port multi-provider), so no backend check here.
|
|
727
835
|
void ensureCommands()
|
|
728
836
|
.then((commands) => {
|
|
729
837
|
const useful = commands.filter((c) => PANEL_SLASH_ALLOWLIST.has(c.name));
|
|
@@ -740,69 +848,109 @@ export async function runPanelOrchestrator() {
|
|
|
740
848
|
// socket is open." A bare/undriven bridge stays silent, so the panel can
|
|
741
849
|
// tell the difference (and warn if no ack arrives).
|
|
742
850
|
if (event.type === "hello" && event.tab_id) {
|
|
743
|
-
|
|
744
|
-
//
|
|
851
|
+
const panelTab = event.tab_id;
|
|
852
|
+
// Retarget ComfyUI to the URL the browser was served from (window.location),
|
|
853
|
+
// BEFORE the readiness probe so the "ready" ack reflects the right instance.
|
|
854
|
+
applyComfyuiUrl(event.comfyui_url);
|
|
855
|
+
// Per-tab backend selection (single-port multi-provider). The panel names
|
|
856
|
+
// its chosen provider on connect (and on a switch it re-sends hello / a
|
|
857
|
+
// set_backend); absent or unknown → the default.
|
|
858
|
+
const reqBackend = typeof event.backend === "string"
|
|
859
|
+
? event.backend.toLowerCase()
|
|
860
|
+
: undefined;
|
|
861
|
+
const backend = reqBackend && KNOWN_BACKENDS.has(reqBackend) ? reqBackend : defaultBackend;
|
|
862
|
+
const prev = tabBackends.get(panelTab);
|
|
863
|
+
if (prev && prev !== backend) {
|
|
864
|
+
// Provider switch: retire the previous provider's agent for this tab so it
|
|
865
|
+
// doesn't linger. The new provider starts a FRESH session (the panel
|
|
866
|
+
// replays the transcript as context on its first message to seed it).
|
|
867
|
+
manager.reset(panelTab + AGENT_KEY_SEP + prev);
|
|
868
|
+
}
|
|
869
|
+
tabBackends.set(panelTab, backend);
|
|
870
|
+
const key = panelTab + AGENT_KEY_SEP + backend;
|
|
871
|
+
// Reload restore: the panel re-sends the last session id it saw. Honored
|
|
872
|
+
// only for a SAME-provider (re)connect — a switch always starts fresh. The
|
|
873
|
+
// orchestrator's own store stays authoritative on the actual spawn.
|
|
745
874
|
const resume = typeof event.resume === "string" ? event.resume : undefined;
|
|
746
|
-
if (resume)
|
|
747
|
-
manager.setResume(
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
875
|
+
if (resume && (!prev || prev === backend))
|
|
876
|
+
manager.setResume(key, resume);
|
|
877
|
+
// Live model list for the picker; SDK slash commands are Claude-only.
|
|
878
|
+
pushModels(panelTab);
|
|
879
|
+
if (backend === "claude")
|
|
880
|
+
pushCommands(panelTab);
|
|
752
881
|
// Re-push the last usage so the context meter isn't blank after a reload.
|
|
753
|
-
const lastStatus = manager.lastStatusFor(
|
|
882
|
+
const lastStatus = manager.lastStatusFor(key);
|
|
754
883
|
if (lastStatus)
|
|
755
|
-
pushStatus(
|
|
756
|
-
const tabId = event.tab_id;
|
|
884
|
+
pushStatus(panelTab, lastStatus);
|
|
757
885
|
const now = Date.now();
|
|
758
|
-
if (now - (lastAckAt.get(
|
|
886
|
+
if (now - (lastAckAt.get(panelTab) ?? 0) < ACK_DEBOUNCE_MS)
|
|
759
887
|
return;
|
|
760
|
-
lastAckAt.set(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
|
|
888
|
+
lastAckAt.set(panelTab, now);
|
|
889
|
+
const isCx = backend === "codex";
|
|
890
|
+
const isGm = backend === "gemini";
|
|
891
|
+
// TRUTHFUL "connected": only claim ready after PROVING the SELECTED backend
|
|
892
|
+
// can run, by probing its model list. If the probe fails — the "connected
|
|
893
|
+
// but dead" wedge — send a degraded ack so the panel shows the real state.
|
|
894
|
+
void ensureModels(backend)
|
|
766
895
|
.then((models) => {
|
|
767
896
|
if (models.length) {
|
|
768
|
-
|
|
769
|
-
// a WS blip, or a real restart (all carry `resume`) — the user already
|
|
770
|
-
// has their thread, so re-greeting is just noise. The ack still fires.
|
|
771
|
-
// Backend-appropriate messaging (P1-2): each provider must name its own
|
|
772
|
-
// account/auth, and the agent label is that provider's model (Codex/
|
|
773
|
-
// Gemini account default when the env override is unset).
|
|
774
|
-
const agentLabel = isCodex
|
|
897
|
+
const agentLabel = isCx
|
|
775
898
|
? (codexModel ?? models[0].value ?? "Codex")
|
|
776
|
-
:
|
|
899
|
+
: isGm
|
|
777
900
|
? (geminiModel ?? models[0].value ?? "Gemini")
|
|
778
901
|
: model;
|
|
902
|
+
// Greet only on a FRESH session (a resume/reconnect already has the thread).
|
|
779
903
|
if (!resume) {
|
|
780
|
-
const readyText =
|
|
904
|
+
const readyText = isCx
|
|
781
905
|
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Codex (ChatGPT) account. Ask away.`
|
|
782
|
-
:
|
|
906
|
+
: isGm
|
|
783
907
|
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google account (Gemini Code Assist). Ask away.`
|
|
784
908
|
: `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
|
|
785
|
-
bridge.push({ type: "say", text: readyText },
|
|
909
|
+
bridge.push({ type: "say", text: readyText }, panelTab);
|
|
786
910
|
}
|
|
787
|
-
bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend
|
|
788
|
-
logger.info(`[panel-orchestrator] tab ${
|
|
911
|
+
bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend }, panelTab);
|
|
912
|
+
logger.info(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) — agent healthy, ready ack`);
|
|
789
913
|
}
|
|
790
914
|
else {
|
|
791
|
-
const degradedText =
|
|
915
|
+
const degradedText = isCx
|
|
792
916
|
? "⚠️ 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."
|
|
793
|
-
:
|
|
917
|
+
: isGm
|
|
794
918
|
? "⚠️ 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."
|
|
795
919
|
: "⚠️ 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.";
|
|
796
|
-
bridge.push({ type: "say", text: degradedText },
|
|
797
|
-
bridge.push({ type: "ack", ok: false, kind: "degraded" },
|
|
798
|
-
logger.warn(`[panel-orchestrator] tab ${
|
|
920
|
+
bridge.push({ type: "say", text: degradedText }, panelTab);
|
|
921
|
+
bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
|
|
922
|
+
logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) but model probe empty — degraded ack`);
|
|
799
923
|
}
|
|
800
924
|
})
|
|
801
925
|
.catch(() => {
|
|
802
|
-
bridge.push({ type: "ack", ok: false, kind: "degraded" },
|
|
926
|
+
bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
|
|
803
927
|
});
|
|
804
928
|
return;
|
|
805
929
|
}
|
|
930
|
+
// Provider switch WITHOUT a reconnect (single-port multi-provider): the panel
|
|
931
|
+
// picked a different backend chip. Retire the old provider's agent, remember
|
|
932
|
+
// the new one, and re-advertise its models. The panel replays the transcript
|
|
933
|
+
// as context on its next message so the fresh provider has the conversation.
|
|
934
|
+
if (event.type === "set_backend" && event.tab_id) {
|
|
935
|
+
const panelTab = event.tab_id;
|
|
936
|
+
const reqBackend = typeof event.backend === "string"
|
|
937
|
+
? event.backend.toLowerCase()
|
|
938
|
+
: "";
|
|
939
|
+
if (!KNOWN_BACKENDS.has(reqBackend)) {
|
|
940
|
+
bridge.push({ type: "ack", ok: false, kind: "set_backend", message: `unknown backend '${reqBackend}'` }, panelTab);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const prev = tabBackends.get(panelTab) ?? defaultBackend;
|
|
944
|
+
if (prev !== reqBackend)
|
|
945
|
+
manager.reset(panelTab + AGENT_KEY_SEP + prev);
|
|
946
|
+
tabBackends.set(panelTab, reqBackend);
|
|
947
|
+
pushModels(panelTab);
|
|
948
|
+
if (reqBackend === "claude")
|
|
949
|
+
pushCommands(panelTab);
|
|
950
|
+
bridge.push({ type: "ack", ok: true, kind: "set_backend", backend: reqBackend }, panelTab);
|
|
951
|
+
logger.info(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} switched backend ${prev} → ${reqBackend}`);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
806
954
|
// Live panel config (currently just the render-stall threshold). Applied
|
|
807
955
|
// immediately, no reconnect — the next turn's watchdog check uses the new
|
|
808
956
|
// value. Sent by the panel on connect and whenever the setting changes.
|
|
@@ -830,13 +978,13 @@ export async function runPanelOrchestrator() {
|
|
|
830
978
|
// makes the SDK session hang on init. (Defense in depth; the panel only
|
|
831
979
|
// sends ids from the live catalog.)
|
|
832
980
|
if (nextModel) {
|
|
833
|
-
const known = await ensureModels().catch(() => []);
|
|
981
|
+
const known = await ensureModels(backendForTab(tabId)).catch(() => []);
|
|
834
982
|
if (known.length && !known.some((m) => m.value === nextModel)) {
|
|
835
983
|
logger.warn(`[panel-orchestrator] ignoring unknown model "${nextModel}" — keeping current`);
|
|
836
984
|
nextModel = undefined;
|
|
837
985
|
}
|
|
838
986
|
}
|
|
839
|
-
const applied = await manager.setOptions(tabId, { model: nextModel, effort: nextEffort });
|
|
987
|
+
const applied = await manager.setOptions(agentKeyFor(tabId), { model: nextModel, effort: nextEffort });
|
|
840
988
|
bridge.push({
|
|
841
989
|
type: "ack",
|
|
842
990
|
ok: true,
|
|
@@ -862,11 +1010,11 @@ export async function runPanelOrchestrator() {
|
|
|
862
1010
|
// look at me") so the agent stops and fixes it instead of running blind.
|
|
863
1011
|
// Everything else (e.g. a finished render's images) is enqueued normally.
|
|
864
1012
|
if (ev.kind === "run_error") {
|
|
865
|
-
void manager.injectRunError(event.tab_id, ev.error ?? "unknown error");
|
|
1013
|
+
void manager.injectRunError(agentKeyFor(event.tab_id), ev.error ?? "unknown error");
|
|
866
1014
|
logger.info(`[panel-orchestrator] tab ${event.tab_id.slice(0, 8)} run_error → agent (interrupt)`);
|
|
867
1015
|
return;
|
|
868
1016
|
}
|
|
869
|
-
const delivered = manager.injectEvent(event.tab_id, ev);
|
|
1017
|
+
const delivered = manager.injectEvent(agentKeyFor(event.tab_id), ev);
|
|
870
1018
|
if (delivered) {
|
|
871
1019
|
logger.info(`[panel-orchestrator] tab ${event.tab_id.slice(0, 8)} event → agent: ${event.kind}`);
|
|
872
1020
|
}
|
|
@@ -880,7 +1028,7 @@ export async function runPanelOrchestrator() {
|
|
|
880
1028
|
// interrupted turn so BOTH messages get answered; a plain Stop/Ctrl+C/Esc
|
|
881
1029
|
// sends a bare interrupt and must NOT re-run the stopped turn.
|
|
882
1030
|
const requeueInFlight = event.requeue === true;
|
|
883
|
-
void manager.interrupt(tabId, { requeueInFlight });
|
|
1031
|
+
void manager.interrupt(agentKeyFor(tabId), { requeueInFlight });
|
|
884
1032
|
bridge.push({ type: "ack", ok: true, kind: "interrupt" }, tabId);
|
|
885
1033
|
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} interrupted${requeueInFlight ? " (send-now: re-queue)" : ""}`);
|
|
886
1034
|
return;
|
|
@@ -890,7 +1038,7 @@ export async function runPanelOrchestrator() {
|
|
|
890
1038
|
if (event.type === "cancel_message" && event.tab_id) {
|
|
891
1039
|
const tabId = event.tab_id;
|
|
892
1040
|
const mid = typeof event.mid === "string" ? event.mid : undefined;
|
|
893
|
-
const removed = mid ? manager.cancelQueued(tabId, mid) : false;
|
|
1041
|
+
const removed = mid ? manager.cancelQueued(agentKeyFor(tabId), mid) : false;
|
|
894
1042
|
bridge.push({ type: "ack", ok: true, kind: "cancel_message", mid, removed }, tabId);
|
|
895
1043
|
return;
|
|
896
1044
|
}
|
|
@@ -900,7 +1048,7 @@ export async function runPanelOrchestrator() {
|
|
|
900
1048
|
const tabId = event.tab_id;
|
|
901
1049
|
// reset() is synchronous (map cleared now), so no concurrent send() can
|
|
902
1050
|
// spawn an agent before we report the cleared session.
|
|
903
|
-
manager.reset(tabId);
|
|
1051
|
+
manager.reset(agentKeyFor(tabId));
|
|
904
1052
|
bridge.push({ type: "session", session_id: null }, tabId);
|
|
905
1053
|
bridge.push({ type: "ack", ok: true, kind: "new_session" }, tabId);
|
|
906
1054
|
return;
|
|
@@ -912,7 +1060,7 @@ export async function runPanelOrchestrator() {
|
|
|
912
1060
|
if (event.type === "rewind" && event.tab_id) {
|
|
913
1061
|
const tabId = event.tab_id;
|
|
914
1062
|
const anchor = typeof event.anchor === "string" ? event.anchor : null;
|
|
915
|
-
const ok = manager.rewind(tabId, anchor);
|
|
1063
|
+
const ok = manager.rewind(agentKeyFor(tabId), anchor);
|
|
916
1064
|
bridge.push({ type: "ack", ok, kind: "rewind" }, tabId);
|
|
917
1065
|
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} rewind (anchor=${anchor ? anchor.slice(0, 8) : "fresh"}, ok=${ok})`);
|
|
918
1066
|
return;
|
|
@@ -923,7 +1071,7 @@ export async function runPanelOrchestrator() {
|
|
|
923
1071
|
const order = Array.isArray(event.order)
|
|
924
1072
|
? event.order.filter((m) => typeof m === "string")
|
|
925
1073
|
: [];
|
|
926
|
-
const ok = manager.reorderQueue(tabId, order);
|
|
1074
|
+
const ok = manager.reorderQueue(agentKeyFor(tabId), order);
|
|
927
1075
|
bridge.push({ type: "ack", ok, kind: "reorder" }, tabId);
|
|
928
1076
|
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} reorder queue (${order.length} mids, ok=${ok})`);
|
|
929
1077
|
return;
|
|
@@ -934,9 +1082,10 @@ export async function runPanelOrchestrator() {
|
|
|
934
1082
|
if (event.type === "resume_session" && event.tab_id) {
|
|
935
1083
|
const tabId = event.tab_id;
|
|
936
1084
|
const sid = typeof event.session_id === "string" ? event.session_id : undefined;
|
|
937
|
-
|
|
1085
|
+
const key = agentKeyFor(tabId);
|
|
1086
|
+
manager.reset(key);
|
|
938
1087
|
if (sid)
|
|
939
|
-
manager.setResume(
|
|
1088
|
+
manager.setResume(key, sid);
|
|
940
1089
|
bridge.push({ type: "ack", ok: true, kind: "resume_session" }, tabId);
|
|
941
1090
|
return;
|
|
942
1091
|
}
|
|
@@ -1000,7 +1149,17 @@ export async function runPanelOrchestrator() {
|
|
|
1000
1149
|
catch (err) {
|
|
1001
1150
|
logger.debug(`[panel-orchestrator] queue-note check failed (ignored): ${err instanceof Error ? err.message : String(err)}`);
|
|
1002
1151
|
}
|
|
1003
|
-
|
|
1152
|
+
// Transcript replay (single-port provider switch): the panel sends the prior
|
|
1153
|
+
// conversation as `context` on the FIRST message to a freshly-switched
|
|
1154
|
+
// provider, so the new backend has the thread (minus internal session data —
|
|
1155
|
+
// thinking/tool traces/cache aren't portable across providers). Prepend it the
|
|
1156
|
+
// same way crash/queue notes are, so it seeds the fresh session's first turn.
|
|
1157
|
+
const replay = typeof event.context === "string"
|
|
1158
|
+
? event.context.trim()
|
|
1159
|
+
: "";
|
|
1160
|
+
if (replay)
|
|
1161
|
+
outText = `${replay}\n\n${outText}`;
|
|
1162
|
+
manager.send(agentKeyFor(event.tab_id), outText, {
|
|
1004
1163
|
title: event.title,
|
|
1005
1164
|
images: event.images,
|
|
1006
1165
|
mid: userMid,
|
|
@@ -1083,9 +1242,11 @@ export async function runPanelOrchestrator() {
|
|
|
1083
1242
|
QueueMonitor.stop();
|
|
1084
1243
|
unsubscribeSecrets();
|
|
1085
1244
|
await manager.stopAll();
|
|
1086
|
-
// Dispose the readiness-probe
|
|
1087
|
-
|
|
1088
|
-
|
|
1245
|
+
// Dispose the readiness-probe backends (kills each Codex/Gemini CLI child).
|
|
1246
|
+
for (const pb of probeBackends.values()) {
|
|
1247
|
+
if (pb.close)
|
|
1248
|
+
await pb.close().catch(() => { });
|
|
1249
|
+
}
|
|
1089
1250
|
// Tear down the loopback panel HTTP MCP (codex/gemini mode only).
|
|
1090
1251
|
if (panelMcpHttp)
|
|
1091
1252
|
await panelMcpHttp.stop().catch(() => { });
|