Roadmap / TODO¶
The standing work list, distilled from the 2026-08 reviews (three-reviewer docs audit, the ADR-0002 adversarial panel, and the fleet conda survey). Items link to the ADR or page that carries their full rationale. Done items stay listed (struck) so the list doubles as a change record.
cuda-wheels¶
-
Requires-Dist curation -- landed 2026-08-21: per-package
requires_distinpackage.ymlreplaces the wheel's metadata in the existing METADATA rewrite, with{LOCAL}/{VER:<folder>}expansion for exact local-version sibling pins; gate C2 asserts the result. Seven packages curated (ovoxel, spconv, cumm, cubvh, sageattn3, detectron2, nvdiffrast); torch lines deliberately untouched. Unblocks lockfile-visible inlining in comfy-env. (CW-ADR-0004) -
Conda torch builds for every python/os/cuda combo PyPI gets. PyTorch publishes no conda packages, and conda-forge's community builds cover roughly 30% of the combo grid PyPI carries -- which is why the torch family rides a separate pip index inside otherwise-conda envs, with all the two-system machinery that implies (Why not just conda?). The farm already builds CUDA wheels per combo; extending it to build conda torch packages for the full PyPI grid would let the solver own torch like any other dependency. Big (torch is the heaviest build in the ecosystem), unscheduled, recorded so the wheel-farm work builds toward it.
comfy-env¶
12.8 on ARM64 -- resolved in the resolver, still blocked in the farm
The tier-2 fallback is now per CPU architecture: cu12.8/torch2.8 on
x86_64, cu13.0/torch2.10 on linux aarch64. The old fallback was
unsatisfiable on ARM -- PyTorch published no aarch64 wheel for the entire
2.8 line on cu128 -- and 12.8/12.9 would have left Thor (sm_110) with no
kernel image. 13.0 is the only line whose ARM arch list carries Thor
natively, and it is where the ARM torchvision/torchaudio wheels become
CUDA-tagged. Requires driver r580+. The wheel probe also matches the
architecture now -- it used to accept an x86_64 wheel on an ARM host and
let pip discover the mismatch much later.
Still outstanding: the farm builds ARM for exactly one pilot package
(cc_torch), so tier 2 on ARM will still miss for any real pack. Widening
platforms: beyond the pilot is a build-capacity decision
(CW-ADR-0015).
- Forbid non-comfy-env'd
[node_packs]peers. A peer that is not itself comfy-env'd installs its ownrequirements.txtstraight into the shared host env -- exactly what the host-env principle (ADR-0003) forbids, done on our behalf. The intent is that every peer either carries its own isolated env or is not a peer. Permitted for now: the ecosystem is not there yet, and refusing today would make[node_packs]unusable for most real packs (a pack depending on KJNodes, say, gets its five deps in the host env). Treat it as a known, temporary leak rather than a supported pattern. - Unscheduled thought (2026-08, not a commitment): if the host env can be detected as conda-managed already, installing a pack's deps into that existing env might one day be acceptable -- recorded from the isolate-flag debate; today the answer to envless remains "not supported".
- Revive the inlining path once curated wheels land -- URL
pypi-dependencies in generated manifests, side-channel +
--no-cacheretired. (Why not just conda?) - Finish the orphaned system-env path --
_collect_root_conda_depsis defined and never called; wire it as the shared GL/ffmpeg runtime layer. Resolved by deletion 2026-08: its only would-be consumers (root-scope[dependencies]in UniRig/HYWM2) duplicated deps their subdir envs already deliver; those root sections were removed too. The shared-system-env idea stays here in case it is ever properly designed. - libomp dedupe beyond macOS; stop blanket
KMP_DUPLICATE_LIB_OK-- the enforcement arm of the lineage-coherence principle (ADR-0002). Needs a design pass before code, because the macOS fix does not transplant:- macOS today:
dedupe_libomp()symlinks every bundledlibomp.dylibin site-packages to torch's canonical copy (environment/libomp.py, run at prestartup and per materialized env). - Windows: symlinks need privileges/dev-mode; needs hardlink-or-copy
semantics plus DLL search-order care (
os.add_dll_directory, Library/bin precedence) -- and the DLL name differs (libiomp5md.dllvs conda'slibomp.dllfamily), so name-matching is part of the design. - Linux: soname variants (
libomp.so.5,libgomp) and RPATH-baked loads; symlinking inside site-packages works but must respect the env's condalibompas canonical when the env is conda-lineage (lineage coherence), torch's copy otherwise. - Exit criterion: once dedupe verifiably runs on all three platforms,
remove
KMP_DUPLICATE_LIB_OK=TRUEfrom[activation.env]in generated manifests and both hand-rolled env builders, so duplicate OMP runtimes fail LOUDLY instead of silently corrupting numerics.
- macOS today:
- ~~Housekeeping:
[apt]/[brew]removed (pre-pixi legacy);workspace.pydocstring now describes reality; dead_read_env_torch_versiondeleted outright~~ -- done. - Support v3
comfy_entrypointregistration in the metadata scan -- the scan reads onlyNODE_CLASS_MAPPINGS, so an isolated package that registers purely viacomfy_entrypoint()(no dict) scans as 0 nodes, silently -- the same whole-pack-vanishes failure the accelerator rule fixed. Measured over the top 500 Registry packs: 30 (6%) are purecomfy_entrypointand they are the big/modern ones (cg-use-everywhere #1, animatediff-evolved, advanced-controlnet, inpaint-nodes, prompt-control); the fraction is growing. - Not urgent under the current opt-in contract (authors route through
register_nodes/initc->NODE_CLASS_MAPPINGS), but cheap and strategically pointed for the "isolate arbitrary packs" direction -- those top packs are exactly the ones one would want to wrap. - Now (~3 lines): detect a pure-
comfy_entrypointpackage in the scan and warn loudly ("registers via comfy_entrypoint, which the scan does not read -- expose NODE_CLASS_MAPPINGS or use register_nodes") instead of returning 0 nodes. - Soon (~20 lines): full scan support -- mirror ComfyUI's loader
(
nodes.py): callcomfy_entrypoint(),await get_node_list(),GET_SCHEMA()per class. The proxy half already exists (_build_v3_proxy_class, proven by GeometryPack's v3 classes), so only the scan's entry-door invocation is missing. - Deferred by design: uv-first materialization for envs with zero conda
content; CI-pre-solved
pixi.lockper env x ABI-tag for the ComfyUI Desktop population; py-rattler (watch item). - ~~Pin the pixi binary (version + sha256, version marker)~~ -- done.
- ~~Canary transport handshake~~ -- done (ADR-0005).
Open items from the 2026-08-15 four-reviewer scan¶
- Resolve the by-reference object cache -- decide, then fix. The
worker keeps non-tensor custom objects resident and returns
{"__comfy_ref__": ...}handles (_persistent_worker.py_cache_object/_serialize_result);_object_cacheis never evicted -- an unbounded leak for the worker's life. It also contradicts ADR-0029, which says by-reference was "killed twice on census evidence." Decide whether the path is live (then cap/TTL it and correct 0029 to scope its claim to the cross-pack plane) or dead (then delete it). Do not fix the leak before deciding, or you entrench a design 0029 says was rejected. - Three-hook upstream RFC to Comfy-Org -- the executable form of the ADR-0024 loan book: propose (as a ComfyUI issue/discussion) three official seams that retire most of the patch surface -- a node-registration seam, a VRAM lease/eviction API, and progress/interrupt forwarding. Strategic, not code; leverage (working system + 50-pack deployment + measurements) depreciates as Comfy-Org builds its own isolation. Optional, but the only item with a shrinking clock.
- Barrages vs suite-monorepo -- decide before the
ADR-0017
rollout tripwire whether the ~24
[node_packs]-linked packs consolidate into one suite repo (atomic barrage = one commit) or stay hand-run. Trivial now, near-impossible after external packs pin independently. - Widen the docs truth-sweep to defect-claims + branch names. The
ADR-0027 doc-claims
sweep greps config keys/env vars against the tree; extend it to flag any
ADR line saying "in flight / on the
<x>branch / still present / currently breaks", since a merge silently turns those into fiction (the 0.4.18 batch left six such lines across five ADRs -- swept 2026-08-15). - Reclaim orphaned
/dev/shmon a double-crash. If both parent and worker are SIGKILLed, a reply's shm blocks leak until reboot -- no ack, no TTL sweep. Extend the startup sweep (wrap.py, which already reaps stale sockets/temp dirs) to orphaned shm blocks (ADR-0032).
comfy-test¶
- Consume ACCELERATOR declarations -- CPU lanes skip tagged nodes honestly (no empty-module mocks for declared packs); derive the cpu/cuda workflow splits from workflow content with the dispatcher-ambiguity rule and manual override (accelerator declarations).
- CI reproducibility -- pin the comfy-test version test jobs install
(no
--upgraderaces); derive the random Python pick fromrun_idso re-runs reproduce; replace the gh-pages force-push with fetch-rebase-retry. - comfy-env contract seam -- stop importing
_abi_tag/ hardcoding the env layout; consume a stablecomfy-env info --jsoninstead. javascriptisolation level -- static collision lint of a pack's frontend JS (reporting/js_lint.py): ComfyUI auto-imports everyweb/**/*.jsinto one shared browser page, so packs collide via global writes, duplicateregisterExtensionnames, unguardedmessagelisteners, shared-object monkeypatches, shared-DOM/storage writes. The level derives the required namespace from[tool.comfy].DisplayNameand errors on any main-realm touch of state a pack does not own;.mjs(iframe-only) is exempt, and a.mjspulled in by a.jsimport is followed. Done 2026-08. Remaining: thread the pack's node names in so a namespaced extension that hooks another pack's node (the squat class) is caught, and land the deferred runtime tier (diffwindow/DOM after load -- catches variable-aliased globals the static pass cannot).
cookiecutter-comfy-extension¶
-
Update the scaffold -- it does not produce a comfy-env pack. cookiecutter-comfy-extension predates the current contract and generates a pack that cannot work. The README advertises it as shipping "the canonical
install.py/prestartup_script.py/__init__.pytriplet"; measured 2026-08-24, none of the three calls appear in it:install.pydoesfrom comfy_env import IsolatedEnvManager-- an API that does not exist. A generated pack raisesImportErroron install against any current comfy-env. It never callsinstall().__init__.pyhand-rollsfrom .src.<slug>.nodes import NODE_CLASS_MAPPINGSinstead of callingregister_nodes(), so the generated pack has no isolation at all -- the opposite of the point.prestartup_script.pycopies assets and never callssetup_env().
The config is equally stale: a single
comfy-env.tomlat the pack root, which is not one of the two supported locations (config reference) and would never be discovered; an obsolete[<slug>]/[<slug>.packages]schema with acuda_version = "auto"key that no longer exists; nocomfy-env-root.toml, so no[node_packs]and no[types]; and nocomfy-test.toml. The frontend half shipsWEB_DIRECTORY = "./web"plusweb/js/example.js-- the dead-declaration trap stripped from 12 packs in August, superseded byjavascript/declared through[tool.comfy] web.Worth doing as a regeneration against a known-good pack rather than a patch: take Hunyuan3D-Part (lightest root-only shape) and GeometryPack (full isolation) as the two targets the template should be able to reproduce, and add a CI job that generates the scaffold and runs
comfy-teston it, so the template cannot rot silently again.
Hypothetical TODO: RAM/VRAM efficiency levers¶
Status: ideas, not commitments. Everything here was measured/discussed on Windows only (2026-08, one machine -- Windows 11, RTX 4060 Ti; numbers in ADR-0001's cost table). The Linux items are unmeasured hypotheses. Re-measure before building any of this.
Ranked by value-per-effort as currently understood:
- Same-volume placement (free; Windows-measured). OS page sharing of torch binaries only works between processes mapping the same file -- measured ~157 MB shared per same-build CUDA worker. Host venvs on D: and the workspace on C: get zero sharing between ComfyUI-main and workers despite identical builds (hardlinks cannot cross volumes). Candidate: an install-time advisory when host env and workspace volumes differ.
- Idle worker reaper (already proposed in ADR-0001). Each env the user stopped touching holds ~550 MB host + ~150 MB VRAM (CUDA context); reap after an idle window, never putting spawn latency back on the execution path.
- Narrow the combo spread (wheel-farm coverage). Every tier-2 fallback env (cu128/torch2.8 beside a cu130/torch2.12 bootstrap) is an unshareable second torch on disk AND in RAM (~400 MB private per worker becomes fully private). Building the missing wheels converts duplication into sharing.
- Linux-only, unmeasured: CUDA MPS (shares context infrastructure
across processes); fork/COW zygote (heap "hardlinking" -- blocked on
Windows, defeated by one-worker-per-env, CUDA does not survive fork);
KSM
madvise(MERGEABLE)(kernel dedupes identical anonymous pages, server-only, CPU cost). File under "if forge ever runs server fleets". - Tensor daemon (endgame, ADR-0010 future work): one GPU-owner process, one CUDA context total; workers become CPU orchestrators. Big redesign; parked.
Irreducible on Windows (do not chase): the per-process Python heap (~250 MB/worker beyond shared pages) and the per-process CUDA context (~125 MB host + ~150 MB VRAM) -- no OS primitive shares either.
Left open by the 2026-08 VRAM repair¶
ADR-0034 and ADR-0035 fixed admission and the proxy's honesty. What they deliberately did not fix:
- Offload latency is now the visible cost. With eviction actually
running, a worker's partial unload is a synchronous IPC round trip plus
a
torch.cuda.empty_cache(). Nobody has profiled how much that adds to a pressured graph. Measure before optimising. - The ledger's blind spot survives. When NVML and
nvidia-smiare both unavailable, the offset comes from comfy-env's hook-based accounting, which cannot see allocations that bypassnn.Module.to()/.cuda(). Admission stays optimistic there. Logged per request with its source so the degraded mode is at least visible. - Multi-GPU is still single-device by assumption. The
devicefield reserved in ADR-0025 is still not on the wire. - Verified by construction and unit test, not by a workload. The measurement that motivated the fix was a synthetic sibling-allocation probe. A real before/after on a pressured multi-env graph has not been run.
Upstream watch items¶
- pixi PR #5464
(per-dependency
no-deps) -- would let manifests carry the wheels with zero farm changes; stalled, worth a nudge. - conda-forge pytorch coverage -- feedstock is current (2.13, CUDA on Linux + Windows); the gate for conda-native publishing is our dependent-package matrix plus the build-lineage caveat for zero-copy. (Why not just conda?)
Frontend JavaScript isolation¶
comfy-env isolates a pack's Python into its own process and environment. Its frontend JS cannot currently be isolated the same way: ComfyUI serves every pack's scripts into one shared browser origin, so there is no per-pack boundary to isolate at. The reasoning and the deferral are ADR-0031.
What ships today is containment, not isolation: the javascript test level
lints for collisions (unnamespaced extensions, foreign node hooks, unguarded
message listeners, shared DOM injection), and iframe-only bundles are kept out
of the shared realm by naming them .mjs, which ComfyUI's **/*.js glob does
not pick up.
Why collisions are possible today¶
The mechanism is three lines of ComfyUI, and none of them is a bug. Together they mean a pack cannot declare a boundary even if it wants one.
1. Every .js under a pack's web directory is imported into the page.
A pack sets WEB_DIRECTORY and that directory is recorded
(nodes.py) and mounted as static at /extensions/<pack>
(server.py). GET /extensions then globs it recursively and returns a
flat list of every file:
files = glob.glob(os.path.join(glob.escape(dir), '**/*.js'), recursive=True)
The frontend imports the whole list into one realm, concurrently, keeping
going past any failure. De minified from comfyui_frontend_package:
await Promise.all(list.filter(e => shouldLoadExtension(e, false)).map(async e => {
try { await import(H.fileURL(e)) }
catch (t) { console.error('Error loading extension', e, t) }
}))
Note the filter. A skip mechanism already exists, and it is core only:
shouldLoadExtension excludes anything under extensions/core plus a
hardcoded Set of two cloud files. Packs cannot add to it.
So there is no entry point. Every file is an entry point, and every one lands
in the same window, the same LiteGraph, the same app.
2. A pack cannot say "this file is not an extension". Helper modules are
imported as though they were. In ComfyUI-KJNodes, four files register nothing
at all (utility.js, fix_node.js, editors/editor_base.js,
editors/interpolation.js); they exist to be imported by siblings. The glob
imports each one anyway. Their siblings show what that costs:
editors/point_editor_canvas.js calls createEditorStylesheet(...) at
module scope, so a <style> element is appended to document.head on every
page load whether or not a spline editor is ever opened.
3. A pack cannot ship an asset that is served but not executed. This is the
one with a public workaround. ComfyUI-KJNodes vendors marked.min.js and
purify.min.js for a help popup most users never open. Rather than let the
glob import both into the realm at startup, it registers a second static
route on ComfyUI's own aiohttp app, pointing outside WEB_DIRECTORY
(__init__.py), with the reason stated in the source:
# NOTE: we add an extra static path to avoid comfy mechanism that loads every script in web.
PromptServer.instance.app.add_routes(
[web.static("/kjweb_async", (Path(__file__).parent.absolute() / "kjweb_async").as_posix())]
)
The files are then pulled in lazily by a hand written loadScript that
de duplicates on document.querySelector('script[src=...]'). That helper
carries the line "code based on mtb nodes by Mel Massadian", so this is a
pattern packs copy from each other rather than a single author's quirk. A pack
had to reach into the server's routing table to avoid the frontend's loader.
4. There is no capability query, so packs read core's source. With one
shared realm and no version negotiation, a pack that needs to know whether the
frontend already supports something has no API to ask. KJNodes stringifies a
core method and greps it (web/js/setgetnodes.js):
const nativeSource = proto.resolveOutput.toString();
const hasNativeSupport = nativeSource.includes('resolveVirtualOutput');
That is fragile twice over. It breaks under minification, and if another pack
patched resolveOutput first then .toString() returns that pack's source
rather than core's, so the answer is silently wrong. The prototype it reaches
for is not reached by name either: the pack finds a subgraph node at runtime,
calls getInnerNodes, and takes constructor off the first result, because
the class is not exported.
None of this is careless work. It is what a careful pack does when the platform offers one realm, no entry point declaration, no asset channel and no capability API.
Upstream ComfyUI: the real frontend boundary (PR to open)¶
The javascript lint detects and contains; it cannot make a same-origin,
full-JS plugin safe -- variable-aliased globals (const w = window) and
same-origin iframe reach-through (parent.X from iframe HTML) are past what
static analysis can see, and the install-weighted majority of popular packs
(rgthree, cg-use-everywhere, Crystools, Easy-Use) legitimately patch the
shared canvas/menubar because ComfyUI offers no sanctioned extension point.
Only core can close this. TODO: open an upstream ComfyUI PR proposing, in
leverage order:
- Entry-point manifest replacing the
**/*.jsauto-import glob -- a pack declares which files enter the main realm; everything else underweb/is a static asset. Makes "what runs in the shared page" an explicit, reviewable list and fixes accidental iframe-internal imports (the.mjstrick is already this, informally). - Sandboxed, port-based iframe-widget API (
app.iframeWidget(node, {src, onMessage})returning aMessageChannelport) -- pairwise ports make cross-pack message bleed structurally impossible (noevent.sourceto forget), andsandbox(opaque origin) makes the boundary real soparent.*reach-through throws. Must re-handshake on iframe reload (a navigated iframe drops its port) and needs a CORS/blob-proxy story for in-iframe/viewfetches under an opaque origin. - Sanctioned hooks for shared surfaces -- registered canvas overlays,
menubar/panel slots, theme tokens, serialization middleware -- each
fault-isolated by core, so deep-integration packs stop monkeypatching
LGraphCanvas.prototype. Hooks compose by construction; monkeypatches only compose if every pack chains correctly (today's n-factorial risk).
This is the frontend twin of comfy-env's isolation before sandboxing (ADR-0011): ship the containment now, propose the real sandbox upstream.