ADR-0014: Pack-extensible serializer registry¶
Status: accepted (as-built, 2026-08) -- declaration superseded by
ADR-0015. The registry mechanism itself
(register_serializer, MRO lookup, wire framing) is unchanged and is what
this ADR is still for.
[serializers] is a parse error today
Declare types in [types] in comfy-env-root.toml and put custom
serializers in <pack>/serialization.py
(ADR-0015). A [serializers] table in a
comfy-env.toml raises a named ValueError at load
(config/__init__.py) telling you exactly that; it is not ignored
and there is no shim. The declaration block further down describes the
superseded form and is kept only so a reader of an old pack can
recognise what they are looking at.
Two other things in that block are also history: the worker-side env
var is COMFY_ENV_SERIALIZER_FILES and carries file paths, not
COMFY_ENV_SERIALIZER_MODULES with importable module names; and
OpaquePayload now materializes (owns) its frames on receipt instead
of holding them verbatim (comfy-env 0.4.15). The pack-prefixed-tag
guidance flipped to type-identity tags for shared library types.
Context¶
Node packs ship domain types the transport cannot know about -- meshes,
point clouds, TRELLIS-style sparse tensors, pack-internal result objects.
Before the registry these took one of two bad paths: the pickle rung
(three copies through a SharedMemory block, numpy/pickle version skew
across deliberately-different envs, and the security debt named in
ADR-0010 item 8), or a hardcoded
branch in the generic walker (SparseTensor -- a domain type living
inside the transport layer, flagged in the 0010 review as a defect).
comfy-env needs packs to move their own types efficiently without the
transport learning them one by one.
Decision¶
A per-process serializer registry (workers/_ipc_shared.py), with pack
modules loaded on both sides of the boundary:
SerializerRegistrymaps type__name__->(tag, serialize)andtag -> deserialize. Lookup matches the exact class name first, then walks the MRO -- registering a base class covers its subclasses.- Public API:
register_serializer(type_name, serialize, deserialize=None, tag=None)
serialize(obj, recurse) -> JSON-safe payload. The recurse callback
routes nested values back through the transport, so tensors and arrays
inside a custom payload take the real shared-memory path: custom types
decompose into schema + tensors, never pickle. deserialize(payload,
recurse) -> obj receives the payload raw and decides which nested parts
to reconstruct.
- Wire frame: {"__shm_custom__": <tag>, "payload": ...} inside the
ordinary metadata tree (ADR-0010
owns the framing around it).
- Pack declaration (comfy-env.toml) -- superseded, see the warning
above; this is what the form was:
[serializers]
modules = ["my_pack.wire_types"]
The listed modules were imported for their registration side effects:
parent-side at register_nodes() (wrap.py), worker-side at startup via
an env var (_persistent_worker.py). [serializers] never reached the
generated pixi manifest.
- OpaquePayload: a side that cannot reconstruct a tag (e.g. the
parent env lacks the pack's deps, or the module failed to import) holds
the frame verbatim; re-serializing emits the identical frame. So
parent-mediated worker-to-worker forwarding works without the parent
ever understanding the type, and module import failures degrade to
opaque pass-through instead of crashing
(ADR-0008 posture).
Consequences¶
- The pickle surface shrinks: this registry is the delivery mechanism for
ADR-0010 item 8 (schema-not-pickle). First planned payloads: trimesh
(as
{vertices, faces, ...}tensors, deleting__shm_trimesh__and the attribute-stripping pickle prep) andSparseTensor(out of the generic walker, into a 20-line pack serializer). - Custom payloads inherit the tensor path's performance: a 2 GB mesh moves as two shared-memory arrays instead of a 3-copy pickle blob.
- Trust: deserializers execute pack-provided code at message-decode time. This adds no new boundary -- the same pack already executes arbitrary code as its nodes -- but it means the transport's safety story remains coupled to ADR-0011's sandboxing scope, and a future sandbox must include registry modules.
- Tag namespace is global, last-registration-wins. Two packs
registering the same tag silently override each other; packs should
prefix tags (
geompack.Mesh, notMesh). A collision check is a possible later hardening. - The registry is per-process with no cross-version negotiation of payload schemas; the transport-version handshake planned in ADR-0010 covers the frame format, while payload compatibility across pack versions is the pack's own responsibility (same status as its node I/O contract).