ADR-0039: The env directory seam¶
Status: accepted (2026-09-08). Supersedes the naming half of ADR-0007; the machine-wide workspace and per-env manifests are unchanged.
Decision¶
An env directory is
<env-name>_<abi-tag>, the seam is an underscore, and the ABI tag keeps its version dots. The tag is a directory name and nothing else, so it is not run through the pixi name sanitizer. Older spellings are adopted where they stand, never renamed and never orphaned.
geometrypack-nodes_py313-torch2.8-cu128
└──────┬─────────┘ └────────┬────────┘
env name ABI tag
<env-name>is the pack folder withComfyUI-stripped and lowercased, plus-<subdir>when the config is not at the pack root (environment/cache.py:get_env_name). Every character outside[a-z0-9-]still collapses to a dash, because this half comes from an untrusted folder name on disk.<abi-tag>ispy<ver>-torch<major>.<minor>-<backend>, backend one ofcu128,rocm63,mps,cpu,notorch. Generated by comfy-env from version numbers and a fixed vocabulary, never from disk, so it does not need sanitizing and does not get it.legacy_dir_names()returns the previous spellings.get_env_manifest_dir,cmd_gc's referenced set and the workspace orphan notice all consult it.
Context¶
The seam used to be a dash, and so did everything else. get_env_name joins
the pack to its config subdirectory with -, _abi_tag joins its own fields
with -, the pack name contains -, and the tag ran through
_sanitize_pixi_name, which rendered a version dot as - too. One character
meant four things:
geometrypack - nodes - py313 - torch2 - 8 - cu128
pack ^ subdir ^ tag fields ^ version dot
Three consequences, in ascending order of seriousness.
It cannot be read. torch2-10 is torch 2.10 or torch 2 build 10, and
nothing in the string says where the pack stops.
It cannot be parsed, and something tried. The macOS libomp dedupe tested
"torch" in libomp against the full path. Every env directory contains
torchN-M, so every candidate was classified as torch's own and the dedupe
never ran on any machine. Fixed at the call site
(environment/libomp.py), but the format invited it.
It can collide. ComfyUI-Foo-Bar with a root config and ComfyUI-Foo
with a config in bar/ both derive foo-bar. Inside one install that is a
hard error (install/workspace.py). Across two installs sharing the
machine-wide root it is silent thrash: each re-derives, the identity seal
mismatches, both rebuild, forever.
The underscore is safe precisely because the sanitizer collapses it out of every name component. It cannot occur in either half by construction.
What this decision is not¶
It is not a size optimisation, and the measurement is worth recording so
nobody re-opens it on those grounds. On Windows the deepest real path inside
an env is a Qt licence dump with nested node_modules, 224 characters below
the env directory. Against a %LOCALAPPDATA% root the shortfall past
MAX_PATH is about 70 characters, and the largest saving any naming scheme
can offer is 34 (deleting the name entirely). No name shortens its way under
the limit. COMFY_ENV_ROOT recovers roughly 43 characters, more than any
naming scheme, and is the lever that actually exists. Hashed and abbreviated
schemes were assessed and rejected: they buy nothing measurable and cost the
one artifact a human reads when deciding what to delete.
Consequences¶
- No rebuild. An env is byte-identical under either spelling: nothing
inside records its own path (no absolute paths in
pixi.lockor the generated manifest), and payloads are hardlinks into the shared rattler cache. Verified: 16 of 16 envs on one machine's live stack adopt, none orphan. - Adopt, do not rename. Renaming a directory whose DLLs a running worker holds open fails on Windows with a sharing violation. There is nothing to win by entering that race, so the old directory is used in place.
- Every consumer of
_env_dir_namemust also consultlegacy_dir_names. Miss one andcomfy-env gcsees a live env under its old spelling, finds it absent from the referenced set, and offers to delete it. This is the sharp edge of the decision and the reason the alias is a named function rather than an inline string. - The alias is removed at 0.6.0, by which point every env has been rebuilt for an unrelated reason at least once.
- The derivation collision is fixed alongside this, not by it. A
separator makes the string readable; it does not stop two packs deriving
one name.
env.stamp.jsonnow recordssource(the pack folder plus the config path inside it) andvalidate_env_stamprefuses a bind when it disagrees, naming both sides. That is deliberately not encoded in the directory name: doing so would lengthen every name to catch a case the stamp catches for free, and would still not stop the two from sharing a directory, only from being confused for one another once they had.
The precedent this is answering¶
The last naming change did not have an alias. Four pre-tag directories
totalling 26 GB, 37 percent of a 70 GB store, were still sitting on the
maintainer's own machine long afterwards, because nothing collects them
automatically and their names no longer matched anything. Those envs could not
be adopted: they predate env.stamp.json and so cannot state their own ABI.
Every env built since can, which is what makes adoption safe now and made it
unsafe then.