Adversarial review caught that a LIFO stack keyed by ``prompt_id`` still
mis-attributes events when queue execution order differs from registration
order: a second submission with the same ``prompt_id`` lands on top of the
stack, so the first prompt's events read the wrong workflow_id while it
runs, and the first's ``unregister`` then pops the second prompt's entry.
Replace the stack with an internal monotonic token. ``post_prompt``
registers metadata and stashes the returned token on ``extra_data`` under
``PROMPT_METADATA_TOKEN_KEY``. ``main.py``'s queue worker pulls the token
out, pins it on ``PromptServer.active_prompt_metadata_token`` for the
prompt's execution, and clears + unregisters in ``finally``. The merge in
``send_sync`` reads the active token, so each prompt's events are merged
with its own metadata regardless of ``prompt_id`` collisions.
- comfy_execution/metadata.py: ``merge_prompt_metadata`` now takes an
active token; registry is ``dict[int, PromptMetadata]``; new
``PROMPT_METADATA_TOKEN_KEY`` constant for the extra_data carrier.
- server.py: ``register_prompt_metadata`` returns a token (or ``None``
when no metadata applies); ``unregister`` takes a token;
``get_active_prompt_metadata`` snapshots the pinned entry.
- main.py: pops the token from extra_data, pins on the server, clears
after the terminal "executing: {node: None}" send.
- execution.py ``PromptQueue``: wipe_queue / delete_queue_item now
unregister by token extracted from each item's extra_data.
- comfy_execution/progress.py: reads workflow_id via
``get_active_prompt_metadata`` rather than per-prompt_id lookup.
- tests: unit tests updated for the token signature, plus a real E2E
test (test_prompt_metadata_e2e.py) that instantiates the actual
PromptServer and verifies same-prompt_id-different-workflow_id
submissions don't cross-attribute.
Verified end-to-end against a live ComfyUI server: two submissions with
identical client-supplied prompt_id but different workflow_id each emit
their full execution event stream (execution_start, execution_cached,
executing, executed, execution_success, progress_state, terminal executing)
with the correct workflow_id top-level. 68 / 68 tests pass.
Two issues raised against the per-prompt metadata registry:
1. Client-supplied prompt_id can collide (post_prompt accepts the id
verbatim). With a flat dict-keyed registry, the second submission
clobbered the first and a single unregister could erase metadata still
needed by the other prompt. Now stored as a LIFO stack per prompt_id —
most recent registration wins on merge, unregister pops one entry, the
key is dropped only when the stack drains.
2. BinaryEventTypes.TEXT (send_progress_text) bypasses the metadata merge
because the payload is bytes, and the wire format has no prompt_id /
workflow_id field. The merge can't fix this without a wire-format
change + frontend feature flag, which is out of scope for FE-745. Inside
scope: default the sid to PromptServer.client_id so other clients no
longer silently receive untagged text frames. Cross-tab isolation
inside a single client still depends on the wire-format follow-up.
- comfy_execution/metadata.py: registry is dict[str, list[PromptMetadata]];
merge_prompt_metadata reads stack[-1]; new resolve_progress_text_sid
helper extracted so the routing default is unit-testable without the
full server import chain.
- server.py: register_prompt_metadata appends to the stack;
unregister_prompt_metadata pops; get_prompt_metadata returns a copy of
the top entry; send_progress_text routes through resolve_progress_text_sid.
- tests: collision LIFO behavior, sid resolution default, and the existing
merge tests updated to the stack shape. 16 new assertions in this file,
104/104 pass overall.
PR #13684 added workflow_id directly to ~9 dict literals across execution.py,
progress.py and main.py, along with executor.workflow_id and
server.last_workflow_id state. It was reverted because the execution layer
should not know about workflow concepts and because a finally-clear race
emitted workflow_id=None on the terminal "executing" frame.
Instead, register per-prompt metadata on PromptServer at submission time
and merge it onto outbound WebSocket payloads inside send_sync. The merge
keys off prompt_id (already present on every execution event), so
execution.py stays workflow-agnostic. Metadata is unregistered in main.py's
queue loop AFTER the terminal executing send, which structurally removes
the race.
- New comfy_execution/metadata.py: PromptMetadata TypedDict +
build_prompt_metadata + merge_prompt_metadata helpers.
- PromptServer: prompt_metadata registry (lock-protected), register on
post_prompt, merge in send_sync, expose get_prompt_metadata.
- jobs.py: extracted extract_workflow_id with strict isinstance guards;
_extract_job_metadata delegates.
- main.py: try/finally around the queue iteration; unregister after the
terminal "executing: {node: None}" send.
- execution.py PromptQueue: drop registry entries on wipe_queue /
delete_queue_item so cancellations don't leak.
- progress.py: look up workflow_id from the server registry for the
per-node nested copies and the binary preview metadata, matching #13684's
wire shape so the frontend needs no changes.
- Tests: tests-unit/server_test/test_prompt_metadata.py covers the merge,
the passthrough cases (no prompt_id, unknown prompt_id, binary payloads),
and the terminal-frame race regression.
After a frontend update (e.g. nightly build), browsers could load
outdated cached index.html and JS/CSS chunks, causing dynamically
imported modules to fail with MIME type errors and vite:preloadError.
Hard refresh (Ctrl+Shift+R) was insufficient to fix the issue because
Cache-Control: no-cache still allows the browser to cache and
revalidate via ETags. aiohttp's FileResponse auto-generates ETags
based on file mtime+size, which may not change after pip reinstall,
so the browser gets 304 Not Modified and serves stale content.
Clearing ALL site data in DevTools did fix it, confirming the HTTP
cache was the root cause.
The fix changes:
- index.html: no-cache -> no-store, must-revalidate
- JS/CSS/JSON entry points: no-cache -> no-store
no-store instructs browsers to never cache these responses, ensuring
every page load fetches the current index.html with correct chunk
references. This is a small tradeoff (~5KB re-download per page load)
for guaranteed correctness after updates.