A malformed slots payload crashed the route with a TypeError and answered
500 in plain text. The round-two fix handled the ABSENCE of slots and not
their SHAPE, so reading slot["id"] off a string went straight through -- and
a refusal the app cannot parse is the failure this endpoint exists to avoid.
The shape check is its own function now, because a malformed REQUEST and an
unfinished PIPELINE are different things and get reported separately.
And a window could still be made unreachable, by resizing rather than
moving. Round two clamped position and said "a window can never be put
somewhere it cannot be recovered from"; size was never bounded, so a corner
drag grew it to 4937px in a 900px viewport and carried its own close and
maximise buttons off the screen, leaving Escape as the only way out.
Capping size to the viewport was not enough either: with x above zero the
right edge still left. Resizing is now capped to the room available from
where the window sits, which is a deliberately different rule from dragging
-- a window may be DRAGGED partly off, because a grabbable strip is
guaranteed and that is ordinary behaviour, but it may not be GROWN past the
edge it is anchored to.
Two rounds of fixes, two rounds of holes in those fixes. Both found only by
attacking the code as it stood after the previous fix rather than stopping
at the report that prompted it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A floating window remembers its position per id, and only the DRAG handler
clamped. So a window moved to the right of a wide screen and reopened on a
narrow one opened entirely off the viewport: no titlebar on screen to grab,
nothing to drag back, and the position remembered -- unrecoverable without
clearing localStorage by hand. Reproduced at x=1400 in an 800px viewport.
The clamp lives in apply() now, so every path goes through it: opening,
restoring a remembered box, coming back from maximised, and the viewport
shrinking under an open window. Also guarded for a viewport narrower than
the grabbable strip, where the old bound went negative and pushed the window
further off rather than back on.
jsdom cannot see any of this -- getBoundingClientRect is zeros there, so a
window off the right edge measures identically to one in the middle. The
three tests are Playwright, and removing the clamp fails two of them.
And POST /api/pipeline could not accept an empty pipeline: `slots or
default()` resurrects the built-ins, because an empty list is falsy. A client
that removed every slot got them handed back. Absent means "use the default";
empty means "there is nothing", and only one of those is a fallback.
One of the tests I first wrote for this asserted len(x) < len(y) + 1, which
is true of almost anything. It counts the pipeline at every step now and
fails if it ever grows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Full control did half of what it said and never said which half. It disabled
the mid-run crash guard and left every value refusal in place, so the stated
purpose -- a one-step schedule, a width the model cannot use, let them and
warn -- did not work. Worse, a rejected value was quietly replaced by the
default, so asking for 999 gave 0.4 with a warning about 999.
Now one switch turns off both: the value is passed through, every problem is
said as a warning naming that the consequences are the user's, and a run made
without the guards is identifiable as one afterwards. A key NO module
declares is still never kept, whatever the setting -- nothing reads it, so
keeping it cannot honour the request, only disguise the typo.
core/graph.py was thoroughly tested and called from nowhere: the claim that
a built-in node can be replaced or removed was a property of a library
nothing invoked. It has routes now, and tests that drive them over a real
socket -- the modifiers node removed and the sampler rewired, the VAE loader
swapped for core's, and both refusals coming back as reasons.
Those routes separate two things the old shape conflated. "refused" means
the edit did not happen. "incomplete" means it did and the pipeline still is
not ready -- an unset file picker on a fresh install is the normal case, not
a failed edit, and showing them together would say the wrong thing about both.
And build() could not see a missing REQUIRED input at all, because the
schema lookup flattened required and optional into one dict. A slot with no
model file built clean and failed only once queued.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
[Log] Alert: MiniMaxH3ImageToVideo node | no "first_image" input is connected
[Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
[Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start
The difference between the middle two is the one that matters. Alert is a
choice you may not have made on purpose. Warning is something that will NOT
do what its name says -- the level v4 needed and did not have, which is how
560 handlers came to swallow a failure and carry on, so a feature could be
switched on, do nothing, and never say so.
Info is not decoration either: "loaded on GPU using sage attention" is how
an attention setting that did nothing gets noticed months before the
pictures look wrong enough to investigate.
Every call site moved -- there is no note() left to be vague through -- and
the loaders now say what they actually loaded rather than only what they
were asked for. Records are kept in a bounded ring as well as printed, so a
long rental session cannot turn the log into a memory leak, and served at
/funpack/api/log so a person can read it somewhere other than a terminal
full of ComfyUI's own output. There is a page for it.
Also dropped the preflight this was traded for: min and max are already
declared and already enforced at both doors, and a second protection layer
around a check that exists is ceremony.
Two mistakes worth keeping: a log line reached into the model's shape to
describe it and could have failed the load it was only narrating; and both
new pages passed `children` to an element that takes `rows`, which the kit
accepted and silently dropped. It throws on an unknown element and not on an
unknown prop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An i2v model shown a sharp reference can satisfy the objective by barely
moving. ALG low-passes the anchor while sigma is high, removing the detail
that shortcut needs, and hands the sharp one back once the picture settles.
It attaches to the model, so it applies to whatever sampler is wired, and it
requires temporal_latent and nothing else -- not "for LTX", not "for H3".
Any model with a time axis and a pinned anchor gets it, including ones
nobody here has heard of. A module with no ComfyUI node of its own: settings
and behaviour, which is what most modules should be.
The packed-layout path is NOT ported. v4 reached into a model-specific
arrangement of the latent to find the video stream; where the anchor is not
a plain [B, C, T, H, W] tensor this stands aside and says so once, rather
than reshaping something it is guessing about.
Tested as far as a machine with no GPU can: the filter really low-passes,
only the anchor frame changes, the choice follows sigma, and the whole thing
runs through ComfyUI's real KSAMPLER with only the innermost loop stubbed --
so the anchor a denoiser sees differs between an early step and a late one.
Whether it looks better needs the rental.
Also guarded route registration. PromptServer.instance only exists once a
server has been built, and importing `server` successfully is not the same
as running inside one -- with ComfyUI on the path the unguarded attribute
took the whole pack down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Derived from latent_format where that is possible, which is every
single-tensor model: SDXL, Wan21 and LTXV all come out matching the numbers
core's own nodes compute, including models that ship after this.
H3 cannot be derived and is the reason modules exist. Its latent is a
NestedTensor of a video and an audio branch, its video channel count is not
the latent_channels its format reports, and its length snaps to a frame
grid. So modules/models/minimax_h3 claims it and builds it, importing the
grid from ComfyUI rather than copying a second definition of it.
Nothing outside that folder names a model, and nothing here assumes video:
a 2-D latent format yields an image latent with no branch anywhere.
Modules reach core through modules/_core.py. ComfyUI imports a pack by file
location, so an absolute `import core` in a module resolves under pytest and
not under ComfyUI -- the node then never registers, silently. That cost the
same bug twice, so there is now a test that loads the pack the way ComfyUI
does, in an isolated subprocess whose cwd is NOT the repo. The first version
of that test passed with the bug reintroduced, because cwd was on sys.path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One declaration does three jobs: renders the panel, carries the defaults, names
the values that come back. A module writes no DOM and reads no DOM.
Validation refuses rather than repairs -- unknown type, a renderer its type does
not have, a label that is not text, a default outside its own bounds, a when
naming a key the module never declared. A module that is wrong does not get a
best-effort panel; it does not load, and says why once.
Ordering is by declared relation and stage, never priority numbers, and a cycle
drops only the modules in it. Compatibility is by trait, never model name.
The shell hands a module { composer, values, on, shell } and nothing else: no
node, no document, no kit internals. It cannot style anything because it is
never given anything that could.
catalogue/hide.html puts one good module and five broken ones through the real
shell and shows the result: one panel, five absent, no placeholders. The lint
fixtures make the scanner prove it catches each rule, so a clean scan means
something.
First slice: modules/system/appearance. Two settings, an optional ui.js, and
clicking Light actually turns the app light.
routes.py had no coverage. register() now takes the route table so the real
routes mount on a throwaway app.
The tests write the request line onto the socket instead of using aiohttp's
TestClient, which normalises the URL before sending -- ask it for
modules/%2e%2e/app/x.css and the server receives a plain app/x.css, so the test
looks like a routing bypass when it is the client tidying up. On the wire the
modules route does match, dots intact, and resolve() refuses it.
Splitting the aiohttp and PromptServer imports: one try around both nulled
web whenever ComfyUI's server module was absent, which is every test run.
Static serving routes through a pure resolve(), so the extension allowlist and
the traversal guard are tested without aiohttp. modules/ serves .js only: a
module cannot ship a stylesheet even if one is written.