Remove stale configurator panels before remount

This commit is contained in:
Ken Simpson
2026-08-24 13:54:59 -07:00
parent ee3f5e17c5
commit 3d33db2ee7
5 changed files with 52 additions and 3 deletions
+1 -1
View File
@@ -230,7 +230,7 @@ Active remote nodes show a small progress panel with per-bar `it/s` rates (LLM n
Remote planning and capacity acquisition count as active queue work even though they happen before the rewritten prompt enters ComfyUI's native execution queue. During Vast startup, the canvas receives readable updates for rental, worker-image download, SSH readiness, and runtime initialization. A provider-side terminal image-pull failure is reported immediately rather than appearing as an empty queue until the startup timeout expires. A rental that never produces an SSH-usable worker is marked unusable and destroyed after the bounded initialization wait so a later workflow cannot silently reuse it.
For workflows using a **Remote Execution Configurator**, that node is also the primary planning surface. As soon as scheduling finishes, the scheduler result is streamed to the panel before any Vast rental or SSH-readiness wait begins. The browser retains that plan across late node-panel mounts, tab refocus, and queue failure, so configuration names, selected targets, predicted completion time, and predicted cost remain inspectable even when capacity never becomes ready. Live preparation and execution status use the same durable event stream. Legacy workflows without a configurator continue to use the global status pill.
For workflows using a **Remote Execution Configurator**, that node is also the primary planning surface. As soon as scheduling finishes, the scheduler result is streamed to the panel before any Vast rental or SSH-readiness wait begins. The browser retains that plan across late node-panel mounts, tab refocus, queue failure, and extension reloads, while removing any orphaned prior panel before remounting. Configuration names, selected targets, predicted completion time, and predicted cost therefore remain inspectable even when capacity never becomes ready. Live preparation and execution status use the same durable event stream. Legacy workflows without a configurator continue to use the global status pill.
While a Modal workflow is active, the global status pill lists this instance's active containers, an estimated accumulated GPU cost with the current burn rate (based on Modal's published GPU prices; excludes CPU, memory, storage, credits, and reservations), and — where the workspace plan allows billing-report access — Modal's actual metered app cost at hourly resolution.
+1
View File
@@ -536,3 +536,4 @@
- ComfyUI DOM widgets may mount after their prompt events arrive or remain temporarily detached during canvas virtualization. Store plan and status independently of the element, replay the newest matching prompt after mount, and set a mounted guard only after `addDOMWidget` succeeds so a transient mount failure remains retryable.
- A persisted Vast lease registry describes controller intent, not live SSH health. Worker readiness loops must log changed probe diagnostics as they occur and include instance/environment identity at the service boundary; otherwise a provider closing the connection during key exchange appears as a silent 15-minute initialization wait.
- Provider `running` plus an assigned SSH endpoint is not sufficient evidence that rented capacity is reusable. If bounded runtime initialization fails, mark the lease draining before cleanup, destroy the owned instance, and preserve the original failure even if destruction itself fails; otherwise every later plan can select the same poisoned registry entry.
- A module-local panel map cannot prove that a ComfyUI node has no existing DOM widget after extension reload. Before retrying a mount whose node flag and current module state disagree, remove the named widget through `node.removeWidget` (and detach its element) so the frontend store is unregistered and the node never accumulates duplicate panels.
+26 -2
View File
@@ -80,6 +80,11 @@ class FakeElement {
replaceChildren(...children) {
this.children = [...children];
}
remove() {
this.removed = true;
this.isConnected = false;
}
}
const fakeBody = new FakeElement("body");
fakeBody.appendChild = (child) => {
@@ -1470,15 +1475,30 @@ modalToggle.registerRemoteConfiguratorPlan(
},
[{ configuration_id: "vast-big", display_name: "Vast Big" }],
);
const stalePanelRoot = new FakeElement("div");
const stalePanelWidget = {
name: "remote_execution_plan",
element: stalePanelRoot,
onRemove() {
this.wasUnregistered = true;
},
};
const lateConfiguratorNode = {
id: 381,
comfyClass: "RemoteExecutionConfigurator",
__remoteConfiguratorPanelMounted: true,
size: [300, 100],
graph: configuratorGraph,
addDOMWidget(_name, _type, root) {
widgets: [stalePanelWidget],
addDOMWidget(name, _type, root) {
this.panelRoot = root;
return {};
const widget = { name, element: root };
this.widgets.push(widget);
return widget;
},
removeWidget(widget) {
widget.onRemove?.();
this.widgets.splice(this.widgets.indexOf(widget), 1);
},
setSize(size) {
this.size = size;
@@ -1488,6 +1508,10 @@ configuratorGraph.nodes.push(lateConfiguratorNode);
modalToggle.mountRemoteExecutionConfiguratorPanel(lateConfiguratorNode);
const retainedPanel = modalToggle.remoteConfiguratorPanels.get("381");
assert.ok(retainedPanel, "a stale mount flag must not prevent a retry");
assert.equal(stalePanelRoot.removed, true);
assert.equal(stalePanelWidget.wasUnregistered, true);
assert.equal(lateConfiguratorNode.widgets.length, 1);
assert.equal(lateConfiguratorNode.widgets[0].element, retainedPanel.root);
assert.equal(retainedPanel.promptId, "plan-before-capacity");
assert.equal(retainedPanel.tableBody.children.length, 1);
assert.equal(retainedPanel.table.hidden, false);
+1
View File
@@ -69,6 +69,7 @@ def test_configurator_renders_out_of_band_plan_and_status() -> None:
assert "function serializedRemoteConfiguratorNodeId(prompt)" in source
assert "function mountRemoteExecutionConfiguratorPanel(node)" in source
assert "function removeStaleRemoteConfiguratorWidgets(node)" in source
assert "function registerPromptConfigurator(promptId, configuratorNodeId)" in source
assert "function registerRemoteConfiguratorPlan(promptId, assignments, configurations)" in source
assert "function renderRemoteConfiguratorStatus(panel, state)" in source
+23
View File
@@ -3481,6 +3481,28 @@ function setRemoteConfiguratorTerminalStatus(promptId, phase, message = null) {
renderRemoteConfiguratorStatus(panel, { phase, statusMessage: message });
}
/**
* Remove Configurator DOM widgets left behind by an older extension module.
* @param {LGraphNode} node
*/
function removeStaleRemoteConfiguratorWidgets(node) {
const staleWidgets = (node.widgets ?? []).filter(
(widget) => String(widget?.name ?? "") === "remote_execution_plan",
);
for (const widget of staleWidgets) {
widget?.element?.remove?.();
if (typeof node.removeWidget === "function") {
node.removeWidget(widget);
continue;
}
widget?.onRemove?.();
const widgetIndex = node.widgets?.indexOf(widget) ?? -1;
if (widgetIndex >= 0) {
node.widgets.splice(widgetIndex, 1);
}
}
}
/**
* Mount the non-serialized planning and status surface on the configurator node.
* @param {LGraphNode} node
@@ -3495,6 +3517,7 @@ function mountRemoteExecutionConfiguratorPanel(node) {
) {
return;
}
removeStaleRemoteConfiguratorWidgets(node);
const root = document.createElement("div");
root.className = "comfy-remote-configurator-panel";
root.innerHTML = `