Skip to content

Concurrent connects into a not-yet-grown socket: make grown sockets name-keyed LWW (paired with comfy-cli) #271

Open

Description

@skishore23

Follow-up from Comfy-Org/comfy-cli#967 (review thread on comfy_cli/workflow_ops.py).

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.

Activity

  1. skishore23 commented on Oct 3, 2026

    @skishore23
    CollaboratorAuthor

    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.

  2. skishore23 commented on Oct 3, 2026

    @skishore23
    CollaboratorAuthor

    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:

    1. A node has the dynamic combo speech set to audio, and speech.audio is connected from source S1.
    2. Concurrently, actor A disconnects speech.audio while actor B connects speech.audio from S2. B's connect is a promoted grow plan since #967.
    3. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions