DynamicOutputs: fix block_execution sizing, harden validation, trim comments

Address code-review feedback:

* Fix pre-existing bug surfaced by this work: a V3 node returning a bare
  ExecutionBlocker (normalized to NodeOutput(block_execution=msg) with no
  positional/named result) was silently dropped in get_output_from_returns.
  block_execution now takes precedence and shapes the blocker tuple to the
  active output count first.
* Reject malformed linked-input source node ids (non-string) up front in
  validate_inputs as 'bad_linked_input' instead of letting them fall into
  the type resolver.
* Schema.validate(): reject duplicate DynamicOutputs group ids.
* Drop a couple of over-defensive / restating comments.

New tests: blocker sizing through both the NodeOutput and bare-blocker paths
for dynamic nodes, and duplicate-group-id schema rejection.

Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Jedrzej Kosinski
2026-06-01 20:48:08 -07:00
parent 54f87a09a5
commit 188065c011
5 changed files with 124 additions and 24 deletions

View File

@@ -420,20 +420,26 @@ def get_output_from_returns(return_values, obj, finalized_outputs=None):
# downstream code treats this as a fixed-shape tuple.
named_result = (
_normalize_named_result(r, finalized_outputs)
if getattr(r, "named", None) is not None
if r.named is not None
else None
)
if r.expand is not None:
# block_execution takes precedence: EXECUTE_NORMALIZED converts a bare
# ExecutionBlocker return into NodeOutput(block_execution=msg) with no
# positional / named result, so we must shape the tuple ourselves.
if r.block_execution is not None:
result = tuple([ExecutionBlocker(r.block_execution)] * expected_count)
if r.expand is not None:
has_subgraph = True
subgraph_results.append((r.expand, result))
else:
results.append(result)
subgraph_results.append((None, result))
elif r.expand is not None:
has_subgraph = True
new_graph = r.expand
result = named_result if named_result is not None else r.result
if r.block_execution is not None:
result = tuple([ExecutionBlocker(r.block_execution)] * expected_count)
subgraph_results.append((new_graph, result))
subgraph_results.append((r.expand, result))
elif named_result is not None or r.result is not None:
result = named_result if named_result is not None else r.result
if r.block_execution is not None:
result = tuple([ExecutionBlocker(r.block_execution)] * expected_count)
results.append(result)
subgraph_results.append((None, result))
else:
@@ -493,9 +499,7 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
execution_list.cache_update(unique_id, cached)
return (ExecutionResult.SUCCESS, None, None)
# Finalize the active output list for this prompt (no-op for static V3 / V1
# nodes). Computed once per execute() call so the three output-shaping paths
# below — initial, pending async resume, pending subgraph resume — all agree.
# Finalize active outputs once so initial / async-resume / subgraph-resume paths agree.
finalized_outputs = None
if issubclass(class_def, _ComfyNodeInternal):
schema = class_def.GET_SCHEMA()
@@ -979,9 +983,17 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None, typ
continue
o_id = val[0]
if not isinstance(o_id, str):
errors.append({
"type": "bad_linked_input",
"message": "Bad linked input, source node id must be a string",
"details": f"{x}, linked_node({val})",
"extra_info": {"input_name": x, "linked_node": val}
})
continue
# Reject links pointing at slot indices outside the upstream node's
# active output list (e.g. stale link after a DynamicOutputs branch
# change). Reports the active count for clearer diagnostics.
# active output count (e.g. stale link after a DynamicOutputs branch
# change).
upstream_output_count = type_resolver.finalized_output_count(o_id)
if not isinstance(val[1], int) or isinstance(val[1], bool) or val[1] < 0 or val[1] >= upstream_output_count:
error = {