Mirrors comfy-rnp-server PR #39 on the client. Adds the surfaces
needed for chained partner provider nodes (Tripo Texture/Refine/
Rig/Retarget/Conversion, Kling VideoExtend, future partner chained
nodes) to pass vendor-native task / video / job IDs through a
workflow with their lineage metadata intact.
Surfaces:
* ``protocol.Capability.IO_TASK_HANDLE = "io:task_handle"`` and
``"task_handle"`` added to ``protocol.HEAVY_TYPES`` so
``is_envelope`` recognises the new envelope.
* ``serialization.TaskHandle`` dataclass with the wire-shape fields
(``vendor``, ``kind``, ``native_id``, ``origin_node_id``,
``parent_chain``). ``__str__`` surfaces ``native_id`` for log /
UI preview without leaking the lineage chain.
* ``serialization.decode_task_handle_envelope`` validates every
field (mirroring server-side ``_validate_task_handle_ref`` so a
bad envelope surfaces the same error message on both sides) and
returns the dataclass. Rejects:
- wrong encoding (only ``vendor_inline`` recognised);
- missing / empty top-level fields;
- comma-union ``kind`` strings (the input-acceptance string used
by some provider nodes, NOT an emitted handle kind);
- non-list ``parent_chain``;
- malformed parent_chain entries (missing fields, non-dict,
comma-union kind in lineage).
* ``serialization.encode_task_handle`` re-encodes the dataclass
back to wire shape — symmetric counterpart of the decoder so a
handle decoded from one provider's output and fed into another
provider's input round-trips losslessly without ``proxy_node``
having to know anything provider-specific.
* ``decode_envelope`` dispatcher routes ``type="task_handle"`` to
the new decoder so the generic ``_deserialize_output`` path picks
it up.
* ``client.CLIENT_CAPABILITIES`` advertises
``Capability.IO_TASK_HANDLE`` so server-side capability-gated
descriptors (whole-descriptor gating — partial-socket synthesis
is rejected because a chain that breaks meaningfully if either
end is missing is worse than no chain at all) negotiate cleanly.
The dataclass return (NOT a plain ``str``) is deliberate: a plain
string would silently drop ``parent_chain`` when the value is
passed through the workflow, breaking the lineage the server uses
for future cascading-replay semantics (deferred to a follow-up PR
alongside server-side replay-cache storage — see PR #39 for the
replay design notes).
Smoke test ``notes/run_task_handle_decoder.py`` mirrors the pattern
used by ``run_model3d_bundle_decoder.py``: stubs out the ``client``
import for the relative ``from . import client as rnp_client`` at
the top of serialization.py, then exercises the 9 contract points
end-to-end. End-to-end check imports the server's
``make_task_handle_envelope`` from the comfy-rnp-server worktree
and verifies the wire shape round-trips losslessly through
decode -> encode.
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e3431-c416-742b-973c-e262ab13f2ff