Commit 4a9ebcc
spellcaster_core: Phase 9 — websocket dispatch + ETN inline transport
Closes the loop on the eval doc's "highest-leverage product
improvement pending" (`_dev_docs/EVAL_LANGGRAPH_COMFYSCRIPT.md` §6
Implement #2). Two distinct wins shipped together:
1. **Kill the /history poll race.** Tight workflows (<2s on warm
cache) routinely complete between the poll loop's 500 ms
ticks; the client either misses the result or sees a stale
empty entry. With ws, ComfyUI pushes an `executing` message
with `node==None` the instant the prompt graph finishes.
2. **Eliminate filesystem round-trip on output.** Pre-Phase-9
output went `SaveImage -> output/foo.png -> /view?filename=foo.png`.
With `SaveImageWebsocket` (ComfyUI core) /
`ETN_SendImageWebSocket` (Acly's pack), image bytes arrive as
binary ws frames on the same socket as the status messages. No
file ever lands in `output/`. Privacy improvement + ~50-200 ms
saved per image.
Pair with `ETN_LoadImageBase64` (Acly's pack) for the input side —
embeds input images as base64 inside the prompt JSON, eliminating
`POST /upload/image` + `GET /view` for input-side round-trip too.
Wire format (binary frames) -- per ComfyUI server.py:
header = struct.pack(">II", event_type, image_format)
frame = header + image_bytes
event_type=1 is preview/output image; image_format is 1=jpg, 2=png,
3=jpeg legacy, 4=webp.
What landed
-----------
`comfyui-spellcaster/spellcaster_core/comfy_ws.py` (NEW, 478 LOC)
* `WSImageFrame` dataclass for binary frames (event/format/bytes)
* `WSDispatchResult` for the full collection from one prompt
* `WSError` hierarchy: `WSUnreachable`, `WSTimeout`,
`WSExecutionError`, `WSDependencyMissing`
* `_build_ws_url`, `_decode_binary_frame`,
`_collect_outputs_from_executed`, `_format_execution_error`
helpers
* `submit_and_listen()` — single entry point. Connects
`/ws?clientId=<uuid>` BEFORE posting `/prompt` (race-free),
then listens until the canonical done signal. Filters
messages by `prompt_id` so other clients' broadcasts don't
leak into the result.
* Lazy import of `websockets.sync.client` so the module loads
even if the package is missing (only fails at submit time
with a clear message).
`comfyui-spellcaster/spellcaster_core/dispatch.py` (modified)
* `DispatchResult` gains `binary_outputs` (list of
(format_name, bytes) tuples) and `transport` ("poll" |
"websocket"). Backward-compat: defaults are empty list and
"poll" so existing callers work unchanged.
* `dispatch_workflow()` gains `use_websocket: bool = False`
(opt-in for Phase 9) and `ws_fallback_to_poll: bool = True`
(graceful degradation on ws failure: imports missing,
connection refused, mid-listen drop).
* Branch path replaces the submit + poll block with
`submit_and_listen()` when ws is enabled. Same DispatchResult
shape on the way out.
* Privacy cleanup pass still runs against any FILE outputs
produced in the same workflow (mixed-mode supported).
* Same `extract_execution_error` / `has_usable_outputs` spirit:
if execution_error fires AND outputs exist, warn + return
partial; if execution_error fires AND no outputs, raise.
`comfyui-spellcaster/spellcaster_core/node_factory.py` (modified)
Three new methods on NodeFactory, mirroring the existing
load_image / save_image pattern:
* `etn_load_image_base64(image_b64)` — input via base64
(Acly's ETN_LoadImageBase64 class, GPL-3 sibling pack)
* `etn_send_image_websocket(images_ref, format="PNG")` —
output via ws binary frame (Acly's ETN_SendImageWebSocket)
* `save_image_websocket(images_ref)` — output via ws binary
frame (ComfyUI core SaveImageWebsocket; no sibling pack
needed, always PNG). Documented as the lic-clean alternative
when the GPL-3 sibling dep is undesirable.
`tests/test_phase9_ws.py` (NEW, 28 tests)
Mocks `websockets.sync.client.connect` so no real ComfyUI
server is needed. Coverage:
* URL building (http->ws, https->wss, trailing slash, no scheme)
* Binary frame decoding (png, jpg, too-short guard,
unknown-event guard)
* `_collect_outputs_from_executed` (images + gifs, empty)
* `submit_and_listen` happy path (text + binary mixed)
* `submit_and_listen` execution_error
* `submit_and_listen` execution_interrupted
* `submit_and_listen` filters other clients' prompt_ids
* `submit_and_listen` passes client_id in /prompt body AND
matching ws URL
* `submit_and_listen` post unreachable / http error paths
* `submit_and_listen` progress callback fires for each stage
* `dispatch_workflow(use_websocket=True)` happy + execution_error
+ partial-success + interrupted
* `dispatch_workflow(use_websocket=True)` ws-failure fallback to
poll path
* `dispatch_workflow(use_websocket=True, ws_fallback_to_poll=False)`
ws-failure raises hard
* `dispatch_workflow()` poll path UNCHANGED — same result shape
minus the new fields' defaults
* NodeFactory ETN methods emit correct class_types
`.gitignore` (modified)
Add `tests/test_phase9_ws.py` to the carve-out whitelist
(matches the convention for canonical shared harnesses; tests/*
is gitignored by default).
Verification
------------
`python tests/test_phase9_ws.py` -> 28/28 passed.
Sibling test sweep -> all unchanged from baseline. The only
failures (test_quality_boost: 3/54, test_video_layer: ImportError)
reproduce on the pre-Phase-9 tree, verified via `git stash` +
retest. Not caused by this change.
Adoption
--------
Opt-in per call:
dispatch_workflow(server, workflow,
use_websocket=True, # turn on ws path
ws_fallback_to_poll=True) # graceful degrade
For full inline-transport (no filesystem):
nf = NodeFactory()
img_id = nf.etn_load_image_base64(b64_input) # input-side
# ... pipeline nodes ...
nf.save_image_websocket([decode_id, 0]) # output-side
workflow = nf.build()
result = dispatch_workflow(server, workflow,
use_websocket=True)
# result.binary_outputs == [("png", <image_bytes>)]
# result.outputs == [] (no file landed)
Default behavior is unchanged (use_websocket=False); existing
build_* / dispatch callers keep the historical poll path until
they opt in. Per the eval doc's "ship the lower-risk transport
upgrade first" guidance, this lands without any caller changes.
Refs:
_dev_docs/EVAL_LANGGRAPH_COMFYSCRIPT.md §6 Implement #2 + §7
_dev_docs/ARCHITECTURAL_STUDY_2026-04-30.md sprint-1 #3 +
research-doc PARTIAL items
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 7188519 commit 4a9ebcc
5 files changed
Lines changed: 1535 additions & 4 deletions
File tree
- comfyui-spellcaster/spellcaster_core
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| 104 | + | |
104 | 105 | | |
105 | 106 | | |
106 | 107 | | |
| |||
0 commit comments