fix(print): promoted widgets print at the host address; auto-grow groups print as links - #816
fix(print): promoted widgets print at the host address; auto-grow groups print as links#816skishore23 wants to merge 7 commits into
Conversation
|
Warning Review limit reachedNext included review available in 43 seconds. View limit detailsLimit details: You’ve used all 4 included reviews currently available. Your 65 included PR review attempts over the past 7 days set your current allowance at 4 reviews per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Included review availability: 2 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 6 reviews per hour. 📝 WalkthroughWalkthroughThe workflow printer now derives value-aware widget names, renders auto-grow groups, resolves promoted subgraph values across nesting levels, preserves stable addresses, and reports promoted widget locations. Tests cover rendering, CLI compatibility, malformed definitions, recursion bounds, and workflow immutability. ChangesWorkflow rendering
Sequence Diagram(s)sequenceDiagram
participant CLI
participant workflow_print.py
participant Catalog
participant PromotedDefinitionIndex
participant RenderedPython
CLI->>workflow_print.py: print workflow
workflow_print.py->>Catalog: derive widget and auto-grow metadata
workflow_print.py->>PromotedDefinitionIndex: resolve promoted values
Catalog-->>workflow_print.py: return widget ordering and active groups
PromotedDefinitionIndex-->>workflow_print.py: return effective host or interior values
workflow_print.py-->>RenderedPython: emit arguments, links, annotations, and headers
Merge Risk: ⚪ Minimal · up to The PR corrects workflow printing for promoted widgets and auto-grow groups, with the supplied verification reporting the full suite passing and lint/format checks clean. No actionable merge-blocking risk remains after normal checks. 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
bfb9b70 to
dea39a1
Compare
74501f3 to
ef0ffc3
Compare
|
@coderabbitai review |
|
|
@coderabbitai review |
|
dea39a1 to
12a8904
Compare
ef0ffc3 to
1e2e466
Compare
Medium, not blocking: Perf:
|
base (kishore/be-10305-…) |
this PR | |
|---|---|---|
render_py wall clock |
41 ms | 251 ms |
cProfile attributes 8.79 s of a 10.4 s profiled render to effective_value. Call counts for a 50-instance version of the same workflow:
| function | calls | needed |
|---|---|---|
promoted.promoted_inputs |
451 | 51 |
promoted.defs_by_id |
401 | 1 |
The 400 extra promoted_inputs calls are all find_promoted re-locating a pi the caller already has.
promoted._effective already takes defs, so this is close to a one-liner — an entry point that skips the name lookup:
def effective_value_for(workflow, instance, sg, pi, graph, defs):
"""`effective_value` when the caller already resolved `pi` and `defs`."""
value = host_value(instance, pi)
return value if value is not UNSET else source_value(workflow, sg, pi, graph, defs)and at the call site:
value = _promoted.effective_value_for(state.workflow, node, sg_def, pi, ctx.graph, state.promoted_defs)Note this drops the ValueError the current except at workflow_print.py:885 catches. That handler is unreachable anyway: pi comes from promoted_inputs(sg_def, …) on the same definition the caller resolved, and _render_nodes only reaches this path when is_subgraph_uuid(t) holds and defs_by_id[t] exists, so neither "not a promoted input" nor "not a subgraph instance" can fire.
Not a blocker. Real templates have a handful of instances, so 250 ms is invisible today. Worth doing before anything starts printing generated or batched workflows.
Rest of the PR checks out. I rendered all six gallery fixtures on base vs this head: every case in the description reproduces, no exceptions, no new warnings. The frontend_injected fix is a real one — LoadVideo(file="sloth_bullfight.mp4", control_after_generate="image") becomes LoadVideo(file="sloth_bullfight.mp4").
…ut, not by name Review on #816: the instance line called `promoted.effective_value(workflow, instance, name, graph)` once per promoted widget, and that entry point re-derives `defs_by_id(workflow)` and relocates the input via `find_promoted` -> `promoted_inputs` — both of which the caller already held (`_State.promoted_defs`, and the enclosing loop's `pi`). On 50 z-image instances that was 451 `promoted_inputs` walks for 51 needed; 200 instances rendered in 251 ms against 41 ms on base. Add `promoted.effective_value_for(workflow, instance, sg, pi, graph, defs)` — host value if materialized, else the interior source value — and make `_effective` delegate to it so the by-name path is unchanged. The printer uses it at the call site. The `ValueError` handler there is dropped: `pi` comes from `promoted_inputs` on the very definition `_render_nodes` resolved (`is_subgraph_uuid(t)` and `defs_by_id[t]` both hold), so neither "not a promoted input" nor "not a subgraph instance" could fire. Pinned by a test that counts `promoted_inputs` / `defs_by_id` calls while rendering 40 instances (<= N+2 and <= 2), plus a unit test that `effective_value_for` never re-walks `promoted_inputs` and agrees with `effective_value` for both the interior-default and host-value cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
335bd91 to
8365663
Compare
|
Addressed in 99428ac — thanks for measuring it.
Print/cql/slots/edit suites: 550 passed; ruff 0.15.15 check/format clean. |
…ut, not by name Review on #816: the instance line called `promoted.effective_value(workflow, instance, name, graph)` once per promoted widget, and that entry point re-derives `defs_by_id(workflow)` and relocates the input via `find_promoted` -> `promoted_inputs` — both of which the caller already held (`_State.promoted_defs`, and the enclosing loop's `pi`). On 50 z-image instances that was 451 `promoted_inputs` walks for 51 needed; 200 instances rendered in 251 ms against 41 ms on base. Add `promoted.effective_value_for(workflow, instance, sg, pi, graph, defs)` — host value if materialized, else the interior source value — and make `_effective` delegate to it so the by-name path is unchanged. The printer uses it at the call site. The `ValueError` handler there is dropped: `pi` comes from `promoted_inputs` on the very definition `_render_nodes` resolved (`is_subgraph_uuid(t)` and `defs_by_id[t]` both hold), so neither "not a promoted input" nor "not a subgraph instance" could fire. Pinned by a test that counts `promoted_inputs` / `defs_by_id` calls while rendering 40 instances (<= N+2 and <= 2), plus a unit test that `effective_value_for` never re-walks `promoted_inputs` and agrees with `effective_value` for both the interior-default and host-value cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99428ac to
23cf0dc
Compare
8365663 to
dce5206
Compare
…ut, not by name Review on #816: the instance line called `promoted.effective_value(workflow, instance, name, graph)` once per promoted widget, and that entry point re-derives `defs_by_id(workflow)` and relocates the input via `find_promoted` -> `promoted_inputs` — both of which the caller already held (`_State.promoted_defs`, and the enclosing loop's `pi`). On 50 z-image instances that was 451 `promoted_inputs` walks for 51 needed; 200 instances rendered in 251 ms against 41 ms on base. Add `promoted.effective_value_for(workflow, instance, sg, pi, graph, defs)` — host value if materialized, else the interior source value — and make `_effective` delegate to it so the by-name path is unchanged. The printer uses it at the call site. The `ValueError` handler there is dropped: `pi` comes from `promoted_inputs` on the very definition `_render_nodes` resolved (`is_subgraph_uuid(t)` and `defs_by_id[t]` both hold), so neither "not a promoted input" nor "not a subgraph instance" could fire. Pinned by a test that counts `promoted_inputs` / `defs_by_id` calls while rendering 40 instances (<= N+2 and <= 2), plus a unit test that `effective_value_for` never re-walks `promoted_inputs` and agrees with `effective_value` for both the interior-default and host-value cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
23cf0dc to
fd2ecc0
Compare
dce5206 to
14978e6
Compare
…ut, not by name Review on #816: the instance line called `promoted.effective_value(workflow, instance, name, graph)` once per promoted widget, and that entry point re-derives `defs_by_id(workflow)` and relocates the input via `find_promoted` -> `promoted_inputs` — both of which the caller already held (`_State.promoted_defs`, and the enclosing loop's `pi`). On 50 z-image instances that was 451 `promoted_inputs` walks for 51 needed; 200 instances rendered in 251 ms against 41 ms on base. Add `promoted.effective_value_for(workflow, instance, sg, pi, graph, defs)` — host value if materialized, else the interior source value — and make `_effective` delegate to it so the by-name path is unchanged. The printer uses it at the call site. The `ValueError` handler there is dropped: `pi` comes from `promoted_inputs` on the very definition `_render_nodes` resolved (`is_subgraph_uuid(t)` and `defs_by_id[t]` both hold), so neither "not a promoted input" nor "not a subgraph instance" could fire. Pinned by a test that counts `promoted_inputs` / `defs_by_id` calls while rendering 40 instances (<= N+2 and <= 2), plus a unit test that `effective_value_for` never re-walks `promoted_inputs` and agrees with `effective_value` for both the interior-default and host-value cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fd2ecc0 to
1ac6315
Compare
14978e6 to
bd27441
Compare
…ut, not by name Review on #816: the instance line called `promoted.effective_value(workflow, instance, name, graph)` once per promoted widget, and that entry point re-derives `defs_by_id(workflow)` and relocates the input via `find_promoted` -> `promoted_inputs` — both of which the caller already held (`_State.promoted_defs`, and the enclosing loop's `pi`). On 50 z-image instances that was 451 `promoted_inputs` walks for 51 needed; 200 instances rendered in 251 ms against 41 ms on base. Add `promoted.effective_value_for(workflow, instance, sg, pi, graph, defs)` — host value if materialized, else the interior source value — and make `_effective` delegate to it so the by-name path is unchanged. The printer uses it at the call site. The `ValueError` handler there is dropped: `pi` comes from `promoted_inputs` on the very definition `_render_nodes` resolved (`is_subgraph_uuid(t)` and `defs_by_id[t]` both hold), so neither "not a promoted input" nor "not a subgraph instance" could fire. Pinned by a test that counts `promoted_inputs` / `defs_by_id` calls while rendering 40 instances (<= N+2 and <= 2), plus a unit test that `effective_value_for` never re-walks `promoted_inputs` and agrees with `effective_value` for both the interior-default and host-value cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1ac6315 to
b07860e
Compare
…the host, or the node linked to it (BE-10305) The frontend (ComfyUI_frontend ADR 0009 "Subgraph promoted widgets use linked inputs", SubgraphNode.ts) represents a promoted widget as a linked subgraph input whose value the HOST instance owns: `widgets_values[i]` on the instance, consumed positionally by the i-th subgraph input that resolves to an interior widget. The interior widget is only the default; "the host/exterior value wins over the interior/source value during repair, persistence, and prompt serialization". The CLI still followed the legacy `properties.proxyWidgets` list into the interior node for every read and write, so on the z-image turbo template `set-widget 57.width 768` edited a value the frontend neither ran nor displayed (scenario 1), an outside PrimitiveInt could not be wired to `57.width` (scenario 2), and `comfy run` / `validate` submitted the interior prompt on post-migration templates — `audio_minimax_music_3` ships an interior caption of '' while the host holds the whole prompt. cql/promoted.py (new) is the host-owned value model, pure over workflow JSON: `promoted_inputs` (the frontend's own rule — first boundary link whose target is a widget-backed input or a nested instance's promoted input; sockets own no slot), `host_value` (quarantined hostValue first, then the positional slot), `effective_value`, `set_host_value` (materializes the host array in declaration order, never touches the definition), and `resolve_write`: `<instance>.<input>` → host; an outside link feeding it → its primitive (PrimitiveInt / PrimitiveString* / legacy PrimitiveNode, through Reroutes), refused with the driver named when a non-primitive computes the value; `<instance>/<inner>.<w>` fed by the subgraph input node → the same host (op carries `redirected_from`); fed by another interior node → refused; anything else → the definition, as before. Legacy proxies that the definition does not declare as inputs keep the interior route. set-widget / set-slot / vary share that resolution. connect materializes a declared input on the instance (with the frontend's `widget` marker) and wires it, type-checked against the declared type. slots advertises promoted widgets at the instance address with the value the frontend runs, flags link-driven widgets (`linked_from`), keeps unpromoted interior widgets reachable (nested instances included) and no longer advertises the interior address behind a promotion. convert_ui_to_api overlays host values onto the expanded interior nodes with the frontend's precedence (outside link, host, interior); inner instances first so an outer host wins. Verified against the workflow_templates corpus: 75/75 instances that carry host values match the positional rule with 0 mismatches. Fixtures are verbatim gallery templates plus a trimmed cloud-catalog object_info. The host-write op carries `promoted.value_index` and the materialized `promoted.host_widgets_values`: the comfy-multi-player applier stores a subgraph instance opaquely and rejects a named write to an uncatalogued class, so the in-app path needs a matching applier branch (tracked on BE-10305). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce; wrap list host values; tolerate untyped declared inputs Review findings on #815. `host_widgets_values` was read from the instance dict captured during resolution — for a nested host inside a definition shared by two instances, apply forks the definition on the way down and writes the copy, so the op carried the pre-write array and an opaque applier would have replaced the host state with stale values. The payload is now read from the instance apply wrote. The converter overlay wraps host values like every other widget value, so a two-item list host value is not read back as a `[node, slot]` link. A declared subgraph input with no usable type reads as unknown ("") rather than the repr "None": connect skips the type check and stamps the slot with the source type instead of refusing a valid wire with a misleading mismatch. The reuse test now proves the earlier link was replaced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t docstrings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…everywhere Review (annehe9) on #815: `resolve_write` already checked that the link id serialized on a promoted input still exists (the frontend drops a dangling link on load, so the input is unlinked and the host value runs), but the converter overlay and `slots` used the raw id — so `set-widget` wrote the host while `comfy run` submitted the interior default and `slots` pointed at a source node that does not exist. One helper, `live_external_link`, now decides for all three; the converter and the slots walk thread the link scope through (the workflow for a top-level instance, the containing definition for a nested one, whose links are dicts rather than arrays). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bd27441 to
d208cf9
Compare
…ups print as links `comfy workflow print` rendered two things wrongly: * A subgraph instance read its promoted widget values positionally off the instance's serialized `inputs[]` entries that carried a `widget` marker — the frontend stores them positionally per widget-backed DECLARED input (ADR 0009, cql.promoted). Post-migration templates therefore printed the caption under `switch=` (audio_minimax_music_3), a boolean under `drop_audio="lanczos"` (api_seedance2_5_video_extend), and a pre-migration template (image_z_image_turbo) showed `text=None` with every other promoted widget invisible — while the interior lines showed only `IN.width`, so the reader had no value and no address to edit. The instance line now prints one argument per declared subgraph input in declaration order: a promoted widget shows its EFFECTIVE value (`promoted.effective_value`: host value, else interior default) under the address `set-widget` takes (`57.width`); an outside link into it prints as that link (`width=primitive_int`), with the same PrimitiveNode/Reroute markers a regular node gets; sockets print their link. A declared input no boundary link backs falls back to `proxyWidgets`, exactly as `slots` does. Interior lines keep `IN.<name>`, and the definition header gains a `promoted widgets: IN.width, … — edit 57.<name>, never 57/<id>.<name>` line. Nested promotion resolves through `promoted_inputs`' bounded recursion. * An auto-grow group (`COMFY_AUTOGROW_V3`, e.g. `model.images` under a dynamic combo) was invisible on an agent-built node (`add_node` writes no grown slots), and a top-level group's base entry printed as a phantom `images=None` link input. The group's grown slots now print as links, one open slot stays visible under the exact name `connect` grows next (`"model.images.image_3": None` — the free trailing slot the frontend itself keeps), and the line comment names the group with its element type and capacity (`model.images grows IMAGE (max 14)`). Groups follow the node's current selection via `Graph.autogrow_groups`; widgets stay at their value-aware names and positions, never shifted. Tests: tests/comfy_cli/test_workflow_print_promoted_autogrow.py (21 cases, written red-first against the gallery fixtures and object_info_nested_autogrow), plus the seedream golden line updated for the group annotation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erate Every schema-less positional entry printed under the name control_after_generate, so an older save's LoadImage upload slot (["a.png", "image"]) printed as control_after_generate="image". Injected slots (upload / audioUI / PREVIEW_3D image) carry the new frontend_injected flag from #809: they are walked for position and never printed, matching slots (which omits them) and every write surface (which refuses them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ut, not by name Review on #816: the instance line called `promoted.effective_value(workflow, instance, name, graph)` once per promoted widget, and that entry point re-derives `defs_by_id(workflow)` and relocates the input via `find_promoted` -> `promoted_inputs` — both of which the caller already held (`_State.promoted_defs`, and the enclosing loop's `pi`). On 50 z-image instances that was 451 `promoted_inputs` walks for 51 needed; 200 instances rendered in 251 ms against 41 ms on base. Add `promoted.effective_value_for(workflow, instance, sg, pi, graph, defs)` — host value if materialized, else the interior source value — and make `_effective` delegate to it so the by-name path is unchanged. The printer uses it at the call site. The `ValueError` handler there is dropped: `pi` comes from `promoted_inputs` on the very definition `_render_nodes` resolved (`is_subgraph_uuid(t)` and `defs_by_id[t]` both hold), so neither "not a promoted input" nor "not a subgraph instance" could fire. Pinned by a test that counts `promoted_inputs` / `defs_by_id` calls while rendering 40 instances (<= N+2 and <= 2), plus a unit test that `effective_value_for` never re-walks `promoted_inputs` and agrees with `effective_value` for both the interior-default and host-value cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b07860e to
5e2e69c
Compare
|
Consolidated: this PR's commits land on main via #820 (the stack head, retargeted to main and rebase-merged so each reviewed commit is preserved individually). Approved content unchanged. |
Why
comfy workflow printis how the agent reads a canvas before editing it. It was wrong for the two node shapes the last two PRs fixed:inputs[](only what the UI happened to show) instead of per widget-backed declared input the way the frontend does. Captured on real gallery templates:audio_minimax_music_3printed the caption underswitch=and hid caption/lyrics/seed/models;api_seedance2_5_video_extendprinteddrop_audio="lanczos"(shifted) and droppedinterpolation/padding_color;image_z_image_turboprintedtext=Nonewith all eight promoted widgets invisible and interior lines with no editable address.GeminiNanoBanana2V2'smodel.imagesgroup was invisible; an agent-builtBatchImagesNodeprinted a phantomimages=None.Change (
comfy_cli/workflow_print.py)promoted.effective_value(host value, else interior default) under the addressset-widgettakes (57.width); an outside link into a promoted input prints as the link; sockets print their link; declared-but-unbacked inputs fall back toproxyWidgetsexactly asslotsdoes.IN.width; the header gainspromoted widgets: IN.text, IN.width, … — each is the instance's own value: edit 57.<name>, never 57/<id>.<name>.Graph.autogrow_groupsfor the node's current selection; grown slots print as links; when no free slot exists and the schema allows one, the next schema name prints as an open slot (the free trailing slot the frontend itself keeps — and exactly whatconnectgrows next); the line comment names the group and element type (model.images grows IMAGE (max 14)). Widgets are never shifted.Red → green
tests/comfy_cli/test_workflow_print_promoted_autogrow.py: 21 cases written first, 17 failed on the base (4 pin already-correct value-aware naming/selector behaviour). One golden line intest_workflow_print.pyupdated for the group annotation.Verification
Full suite 6259 passed, 38 skipped; ruff 0.15.15 check + format clean.
🤖 Generated with Claude Code