Skip to content

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

  1. Requires-Dist curation -- landed 2026-08-21: per-package requires_dist in package.yml replaces 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)

  2. 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).

  1. Forbid non-comfy-env'd [node_packs] peers. A peer that is not itself comfy-env'd installs its own requirements.txt straight 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.
  2. 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".
  3. Revive the inlining path once curated wheels land -- URL pypi-dependencies in generated manifests, side-channel + --no-cache retired. (Why not just conda?)
  4. Finish the orphaned system-env path -- _collect_root_conda_deps is 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.
  5. 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 bundled libomp.dylib in 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.dll vs conda's libomp.dll family), 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 conda libomp as 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=TRUE from [activation.env] in generated manifests and both hand-rolled env builders, so duplicate OMP runtimes fail LOUDLY instead of silently corrupting numerics.
  6. ~~Housekeeping: [apt]/[brew] removed (pre-pixi legacy); workspace.py docstring now describes reality; dead _read_env_torch_version deleted outright~~ -- done.
  7. Support v3 comfy_entrypoint registration in the metadata scan -- the scan reads only NODE_CLASS_MAPPINGS, so an isolated package that registers purely via comfy_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 pure comfy_entrypoint and they are the big/modern ones (cg-use-everywhere #1, animatediff-evolved, advanced-controlnet, inpaint-nodes, prompt-control); the fraction is growing.
  8. 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.
  9. Now (~3 lines): detect a pure-comfy_entrypoint package 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.
  10. Soon (~20 lines): full scan support -- mirror ComfyUI's loader (nodes.py): call comfy_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.
  11. Deferred by design: uv-first materialization for envs with zero conda content; CI-pre-solved pixi.lock per env x ABI-tag for the ComfyUI Desktop population; py-rattler (watch item).
  12. ~~Pin the pixi binary (version + sha256, version marker)~~ -- done.
  13. ~~Canary transport handshake~~ -- done (ADR-0005).

Open items from the 2026-08-15 four-reviewer scan

  1. 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_cache is 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.
  2. 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.
  3. 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.
  4. 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).
  5. Reclaim orphaned /dev/shm on 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

  1. 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).
  2. CI reproducibility -- pin the comfy-test version test jobs install (no --upgrade races); derive the random Python pick from run_id so re-runs reproduce; replace the gh-pages force-push with fetch-rebase-retry.
  3. comfy-env contract seam -- stop importing _abi_tag / hardcoding the env layout; consume a stable comfy-env info --json instead.
  4. javascript isolation level -- static collision lint of a pack's frontend JS (reporting/js_lint.py): ComfyUI auto-imports every web/**/*.js into one shared browser page, so packs collide via global writes, duplicate registerExtension names, unguarded message listeners, shared-object monkeypatches, shared-DOM/storage writes. The level derives the required namespace from [tool.comfy].DisplayName and errors on any main-realm touch of state a pack does not own; .mjs (iframe-only) is exempt, and a .mjs pulled in by a .js import 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 (diff window/DOM after load -- catches variable-aliased globals the static pass cannot).

cookiecutter-comfy-extension

  1. 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__.py triplet"; measured 2026-08-24, none of the three calls appear in it:

    • install.py does from comfy_env import IsolatedEnvManager -- an API that does not exist. A generated pack raises ImportError on install against any current comfy-env. It never calls install().
    • __init__.py hand-rolls from .src.<slug>.nodes import NODE_CLASS_MAPPINGS instead of calling register_nodes(), so the generated pack has no isolation at all -- the opposite of the point.
    • prestartup_script.py copies assets and never calls setup_env().

    The config is equally stale: a single comfy-env.toml at 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 a cuda_version = "auto" key that no longer exists; no comfy-env-root.toml, so no [node_packs] and no [types]; and no comfy-test.toml. The frontend half ships WEB_DIRECTORY = "./web" plus web/js/example.js -- the dead-declaration trap stripped from 12 packs in August, superseded by javascript/ 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-test on 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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".
  5. 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-smi are both unavailable, the offset comes from comfy-env's hook-based accounting, which cannot see allocations that bypass nn.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 device field 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:

  1. Entry-point manifest replacing the **/*.js auto-import glob -- a pack declares which files enter the main realm; everything else under web/ is a static asset. Makes "what runs in the shared page" an explicit, reviewable list and fixes accidental iframe-internal imports (the .mjs trick is already this, informally).
  2. Sandboxed, port-based iframe-widget API (app.iframeWidget(node, {src, onMessage}) returning a MessageChannel port) -- pairwise ports make cross-pack message bleed structurally impossible (no event.source to forget), and sandbox (opaque origin) makes the boundary real so parent.* 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 /view fetches under an opaque origin.
  3. 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.