You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Concurrent connects into a not-yet-grown socket: make grown sockets name-keyed LWW (paired with comfy-cli) #271
Problem. Two connect ops can be planned concurrently against a node that doesn't have the target socket yet. A dynamic-combo link input, or a widget→input grow such as model.prompt, is one example. Both appliers then grow the socket under a collision-renamed name: comfy-cli workflow_ops.py, and this package's growInputSlot / nextAutogrowName. Both appliers rename the same way, so the replicas converge. But one of the two concurrent links lands on the renamed socket instead of the intended one. A sequential reconnect is unaffected, because it finds the concrete socket and reuses it.
Proposed fix. Make a grown socket a last-writer-wins register keyed by its name, the way a promoted grow already is. The change has to be made in both appliers at once. Changing only one would break convergence between the CLI and the doc host.
Tests to add. A cross-applier replay test: two concurrent connects into the same not-yet-grown socket, replayed in both arrival orders. It should assert the same final graph on both sides and that neither link is dropped.
Narrowed by Comfy-Org/comfy-cli#967 (8a7ab1b25d). For dynamic-combo link sub-inputs (e.g. speech.audio), comfy-cli now emits the grow with promoted: true, which this applier already treats as one name-keyed LWW register (writeTarget / claimPromotedInput). I ran CLI-minted concurrent connects in both orders through mint → applyOps → project here: the projections are identical and match the CLI, and without the marker they reproduce the speech.speech0 phantom. So that case needs no change in this repo. This issue stays open for the remaining case, the plain widget→input grows such as model.prompt.
Two more convergence risks between comfy-cli's op applier and this applier. Both turned up while fixing Comfy-Org/comfy-cli#967. Neither is fixed in the open PRs. The code references are comfy-cli kishore/agent-edit-contract-iter2 @ 4c6578b26e and comfy-multi-player main.
1. A disconnect on a dynamic-combo link input uses different LWW registers on the two sides
comfy-multi-player (src/applier.ts, disconnectTargetKey): when the link's recorded destination is promoted, the disconnect claims ["input", to_node, "grow", <full name>], the same register the promoted connect claimed. An autogrow destination claims ["input", to_node, "grow", <name up to the first dot>]. Anything else uses the concrete-slot key (stampTargetKey).
comfy-cli (comfy_cli/workflow_ops.py, _write_target): there is no disconnect case. A disconnect falls through to return (kind,), i.e. ("disconnect",). That is a single target shared by every disconnect in the graph, and it never matches the connect's ("input", to_node, "grow", "speech.audio").
Repro:
A node has the dynamic combo speech set to audio, and speech.audio is connected from source S1.
Concurrently, actor A disconnects speech.audio while actor B connects speech.audio from S2. B's connect is a promoted grow plan since #967.
Apply A→B and B→A on each side.
In comfy-multi-player the two ops contend on one register and LWW picks the same winner in both orders. In comfy-cli they never contend, so the result follows apply order: B→A leaves the input disconnected, and A→B leaves it connected to S2. Each replica is internally consistent, but the CLI and the doc host can disagree. Separately, any two unrelated disconnects anywhere in the graph contend in comfy-cli's conflict detection (detect_conflict).
Suggested fix: give _write_target a disconnect case that mirrors disconnectTargetKey: the promoted name register, the autogrow group register, or the concrete slot. Add a cross-applier replay test covering the A/B pair in both orders.
2. The autogrow group name is cut at different dots
comfy-multi-player:nextAutogrowName uses requested.split(".", 1)[0], and disconnectTargetKey uses destination.request.name.split(".", 1)[0]. Both cut at the first dot.
comfy-cli:_autogrow_base uses slot_name.rsplit(".", 1)[0]. It cuts at the last dot, so a group nested under a dynamic combo keeps its own name. _write_target uses the same split for a non-promoted grow.
Repro: a node whose dynamic combo model exposes two autogrow groups, model.reference_images (elements image_1, …) and model.reference_videos.
Collision renaming: two concurrent connects both request model.reference_images.image_1.
comfy-cli renames the loser within model.reference_images → model.reference_images.image_2.
comfy-multi-player's base is model, so it renames to model.<elem> (for example model.model0 with no template), a slot that doesn't exist in that group.
Register sharing: concurrent grows into model.reference_images and model.reference_videos.
comfy-multi-player: both share ["input", n, "grow", "model"], so they contend and one can be dropped or reordered.
comfy-cli: they are two distinct registers, so they don't contend.
Suggested fix: cut at the last dot in this applier too (name.slice(0, name.lastIndexOf("."))), in both nextAutogrowName and disconnectTargetKey, and pin it with the model.reference_images / model.reference_videos pair in a shared replay fixture. Neither side has a nested-group regression test today.
Activity
skishore23 commentedon Oct 3, 2026
Narrowed by Comfy-Org/comfy-cli#967 (8a7ab1b25d). For dynamic-combo link sub-inputs (e.g.
speech.audio), comfy-cli now emits the grow withpromoted: true, which this applier already treats as one name-keyed LWW register (writeTarget/claimPromotedInput). I ran CLI-minted concurrent connects in both orders throughmint → applyOps → projecthere: the projections are identical and match the CLI, and without the marker they reproduce thespeech.speech0phantom. So that case needs no change in this repo. This issue stays open for the remaining case, the plain widget→input grows such asmodel.prompt.skishore23 commentedon Oct 3, 2026
Two more convergence risks between comfy-cli's op applier and this applier. Both turned up while fixing Comfy-Org/comfy-cli#967. Neither is fixed in the open PRs. The code references are comfy-cli
kishore/agent-edit-contract-iter2@ 4c6578b26e and comfy-multi-playermain.1. A disconnect on a dynamic-combo link input uses different LWW registers on the two sides
src/applier.ts,disconnectTargetKey): when the link's recorded destination ispromoted, the disconnect claims["input", to_node, "grow", <full name>], the same register the promoted connect claimed. Anautogrowdestination claims["input", to_node, "grow", <name up to the first dot>]. Anything else uses the concrete-slot key (stampTargetKey).comfy_cli/workflow_ops.py,_write_target): there is nodisconnectcase. A disconnect falls through toreturn (kind,), i.e.("disconnect",). That is a single target shared by every disconnect in the graph, and it never matches the connect's("input", to_node, "grow", "speech.audio").Repro:
speechset toaudio, andspeech.audiois connected from source S1.speech.audiowhile actor B connectsspeech.audiofrom S2. B's connect is a promoted grow plan since #967.In comfy-multi-player the two ops contend on one register and LWW picks the same winner in both orders. In comfy-cli they never contend, so the result follows apply order: B→A leaves the input disconnected, and A→B leaves it connected to S2. Each replica is internally consistent, but the CLI and the doc host can disagree. Separately, any two unrelated disconnects anywhere in the graph contend in comfy-cli's conflict detection (
detect_conflict).Suggested fix: give
_write_targetadisconnectcase that mirrorsdisconnectTargetKey: the promoted name register, the autogrow group register, or the concrete slot. Add a cross-applier replay test covering the A/B pair in both orders.2. The autogrow group name is cut at different dots
nextAutogrowNameusesrequested.split(".", 1)[0], anddisconnectTargetKeyusesdestination.request.name.split(".", 1)[0]. Both cut at the first dot._autogrow_baseusesslot_name.rsplit(".", 1)[0]. It cuts at the last dot, so a group nested under a dynamic combo keeps its own name._write_targetuses the same split for a non-promoted grow.Repro: a node whose dynamic combo
modelexposes two autogrow groups,model.reference_images(elementsimage_1, …) andmodel.reference_videos.model.reference_images.image_1.model.reference_images→model.reference_images.image_2.model, so it renames tomodel.<elem>(for examplemodel.model0with no template), a slot that doesn't exist in that group.model.reference_imagesandmodel.reference_videos.["input", n, "grow", "model"], so they contend and one can be dropped or reordered.Suggested fix: cut at the last dot in this applier too (
name.slice(0, name.lastIndexOf("."))), in bothnextAutogrowNameanddisconnectTargetKey, and pin it with themodel.reference_images/model.reference_videospair in a shared replay fixture. Neither side has a nested-group regression test today.