|
Download ComfyUI-MiniMaxH3-Context-Loop/ComfyUI-MAINodes/concept_lab/DECISIONS.md from dzmdd/Fast_deploy: direct link, hf CLI and curl.
- Browser
- Download file 28.4 kB
-
https://huggingface.co/dzmdd/Fast_deploy/resolve/main/ComfyUI-MiniMaxH3-Context-Loop/ComfyUI-MAINodes/concept_lab/DECISIONS.md
- Command line
-
hf download hf://dzmdd/Fast_deploy/ComfyUI-MiniMaxH3-Context-Loop/ComfyUI-MAINodes/concept_lab/DECISIONS.md
-
curl -L -o DECISIONS.md https://huggingface.co/dzmdd/Fast_deploy/resolve/main/ComfyUI-MiniMaxH3-Context-Loop/ComfyUI-MAINodes/concept_lab/DECISIONS.md
28.4 kB
| # Concept Lab decisions | |
| Append only. Never edit an old entry: add a new one with `supersedes:` | |
| instead. An edited history means the next person re-litigates a settled | |
| argument without knowing they are doing it, and the path looking straighter | |
| than it was is precisely the damage. | |
| Format: `DEC-CL-nnnn · STATUS · date · title`, then Decision / Alternatives | |
| / Consequences. | |
| --- | |
| ## DEC-CL-0001 · ACCEPTED · 2026-08-15 · Fresh subsystem inside MAINodes, named `concept_lab/` | |
| **Decision.** Functional-conditioning work lands as a new package inside | |
| ComfyUI-MAINodes rather than as an extension of any existing | |
| reference-format or adapter-format code. The name is `concept_lab/`. | |
| **Alternatives.** `concept_surgery/` (the source bundle's first suggestion, | |
| and it carries the ancestor program's vocabulary) — not chosen; it reads | |
| sharper than the work is, on a public node pack. | |
| `functional_conditioning/` — most accurate, longest imports, unwieldy node | |
| names. Operator chose `concept_lab/` to sit alongside Motion Lab and Voice | |
| Lab: a place you work rather than an operation you perform. | |
| **Consequences.** Backend-specific formats are compiler *targets*, not core | |
| types. Node display names will read "MAI Concept ...". | |
| --- | |
| ## DEC-CL-0002 · ACCEPTED · 2026-08-15 · The functional delta is the primary object | |
| **Decision.** The primary scientific object is the matched change in model | |
| function caused by a conditioning intervention, not the content of the | |
| source asset and not a similarity between latents. | |
| **Consequences.** Source-latent similarity alone can never certify a | |
| semantic factor. Every factor claim has to reach an effect on the target | |
| stream. | |
| --- | |
| ## DEC-CL-0003 · ACCEPTED · 2026-08-15 · Model-neutral core, H3 as first backend | |
| **Decision.** Core data and factor APIs know nothing model-specific. | |
| Segment names, block counts, sampler shapes and layout rules live behind | |
| `backends/`. H3 is first because MAINodes already has H3 tooling and H3 has | |
| a native image/video/audio reference path. | |
| **Consequences.** `types.py` treats segment names as opaque strings. The | |
| second backend should not require rewriting the first one's evidence. | |
| --- | |
| ## DEC-CL-0004 · ACCEPTED · 2026-08-15 · Three arms, because a reference changes more than its content | |
| **Decision.** First reference experiments capture three arms: actual | |
| reference `R`, shape-matched control `N`, and no reference `0`. | |
| `Delta_content = R - N` is the primary object; `N - 0` and `R - 0` are kept | |
| as diagnostics. | |
| **Why.** Verified against the installed ComfyUI: `PackedLayout` | |
| (`comfy/ldm/minimax/model.py:300`) advances its cursor past every reference | |
| block before placing the target streams, and references also enter the | |
| Qwen3-VL presentation path. So a two-arm on/off difference mixes semantic | |
| content, the presence of a reference, and a moved time origin for the | |
| target stream. | |
| **Consequences.** `N` is a structural control, not a metaphysical zero. | |
| Several control strata must be tried and the sensitivity reported before | |
| `R - N` is called canonical. `backends/h3.py:plan_arms` returns the strata | |
| rather than picking one. | |
| --- | |
| ## DEC-CL-0005 · ACCEPTED · 2026-08-15 · Capture the target stream, not only the reference rows | |
| **Decision.** v0 captures include target-video DiT state after selected | |
| blocks. Reference-row state is secondary diagnostic evidence. | |
| **Why.** The question is not how the reference is represented. It is how | |
| the reference changes what gets generated. | |
| --- | |
| ## DEC-CL-0006 · ACCEPTED · 2026-08-15 · Ids are computed over an explicit identity payload | |
| **Decision.** Every id is a hash of a named identity payload, never of the | |
| whole record. Adding a field to a manifest must not move ids that already | |
| exist on disk. If a new field genuinely belongs to a capture's identity, it | |
| joins `identity()` **and** `PROTOCOL_VERSION` is bumped in the same commit, | |
| deliberately, with an entry here. | |
| **Also decided.** Floats are rounded to 12 significant digits before | |
| hashing, so a sigma arriving as float32 and the same sigma arriving as | |
| float64 name one condition rather than two. | |
| **Alternatives.** Hashing the whole record — rejected: every bank on disk | |
| goes stale the next time anyone adds a note field, for no scientific | |
| reason. Ignoring floats' representation — rejected: an id that disagrees | |
| about whether two identical conditions are identical is an id that lies. | |
| **Consequences.** `tests/test_concept_lab_contracts.py` check 2 asserts | |
| both directions: identity fields move the id, notes and timestamps and | |
| status do not. | |
| --- | |
| ## DEC-CL-0007 · ACCEPTED · 2026-08-15 · Same id with different content is refused | |
| **Decision.** Writing a record under an existing id with different bytes | |
| raises. Writing identical content is idempotent. | |
| **Why.** A capture id names a condition. If the same condition produced | |
| different bytes, one of the two runs is not what its manifest says, and | |
| silently keeping the newer one destroys the only evidence that something is | |
| wrong. | |
| --- | |
| ## DEC-CL-0008 · ACCEPTED · 2026-08-15 · Two-root workspace, neither root in the repo | |
| **Decision.** Manifests default under the ComfyUI output directory | |
| (`output/h3_concept`), tensors default to `/mnt/weights/ai/concept-space/tensors` | |
| on a box that has it, falling back to `<index root>/tensors`. Both are | |
| overridable per call and by `H3_CONCEPT_INDEX_DIR` / `H3_CONCEPT_TENSOR_DIR`. | |
| **Why.** Manifests are for reading and diffing and belong where a human | |
| browses. Tensors are scientific evidence, can be large, and belong on the | |
| array. Neither belongs in a public repository, because a concept pack can | |
| encode a likeness and the failure mode is somebody running `git add -A`. | |
| --- | |
| ## DEC-CL-0009 · ACCEPTED · 2026-08-15 · Imported evidence keeps its origin and never merges | |
| **Decision.** An imported space lands under `imported/<origin space id>/` | |
| with the origin stamped on each record. Imported records are never | |
| re-exported under this space's id. Importing your own export back into | |
| itself is refused. | |
| **Why.** The whole subsystem turns measurements into claims. Somebody | |
| else's measurement informing your work is good; becoming indistinguishable | |
| from your own is how a claim loses its scope. | |
| --- | |
| ## DEC-CL-0010 · ACCEPTED · 2026-08-15 · Packs are private by default; export names what it held back | |
| **Decision.** `ConceptPackManifest.visibility` defaults to `private`. | |
| Export excludes private packs unless explicitly asked, and reports every | |
| exclusion with its reason. Source assets are referenced by content hash and | |
| never copied into a pack. | |
| **Consequences.** The MCP surface can export the shareable set and can see | |
| what was withheld; releasing a private pack is a human keystroke through | |
| the CLI. A model cannot prove who is asking. | |
| --- | |
| ## DEC-CL-0011 · ACCEPTED · 2026-08-15 · Three surfaces, one verb layer | |
| **Decision.** `api.py` holds one implementation of every operation and | |
| returns plain JSON-able dicts. `cli.py`, `mcp.py` and a future `nodes.py` | |
| are thin clients with no rules in them. No tensors, ComfyUI types or torch | |
| objects cross the api boundary. | |
| **Why.** It is what lets an agent drive the subsystem over a pipe, lets the | |
| tests run with no GPU, and stops the three surfaces drifting into three | |
| implementations. | |
| **Consequences.** When a verb eventually touches tensors it returns | |
| references, not handles. If that becomes painful, it gets decided here | |
| rather than worked around in one surface. | |
| --- | |
| ## DEC-CL-0012 · ACCEPTED · 2026-08-15 · Unbuilt verbs refuse and name their task | |
| **Decision.** A verb whose machinery does not exist raises | |
| `NotYetImplemented` carrying the task id that blocks it. It never returns | |
| an empty or placeholder result. | |
| **Why.** The output of this subsystem is evidence. A quiet nothing gets | |
| recorded and later read as a measurement. | |
| --- | |
| ## DEC-CL-0013 · ACCEPTED · 2026-08-15 · No ComfyUI nodes until there is something to run | |
| **Decision.** `concept_lab` registers no nodes and the pack's `__init__.py` | |
| is untouched. Nodes arrive at T12, wrapping proven verbs. | |
| **Why.** Existing MAINodes behaviour and defaults must not move for | |
| additive alpha work. And a half-built research surface in a UI is how ten | |
| unstable knobs become somebody's saved workflow. | |
| **Consequences.** The guarded-import line that adds `concept_lab.nodes` to | |
| the pack's loader is a one-liner when the time comes; the loader already | |
| does exactly this for `window_loop`, `window_expand` and `timeline.nodes`. | |
| --- | |
| ## DEC-CL-0014 · ACCEPTED · 2026-08-15 · Held-out, adversarial and null splits are not optional | |
| **Decision.** A corpus carries four splits and `corpus_status` reports the | |
| missing ones as warnings. No factor ships on extraction-set scores. | |
| **Status of the enforcement.** The warnings exist; a *containment* check | |
| (no source hash appearing in two splits) does not yet, and is proposed in | |
| the review brief. Recorded here so the gap is visible rather than assumed | |
| closed. | |
| --- | |
| ## DEC-CL-0015 · ACCEPTED · 2026-08-15 · Process launch is an identity dimension of a capture; arms of one comparison share one launch | |
| **Decision.** `FunctionalCaptureManifest` carries a | |
| `process` fingerprint and `process["launch_id"]` joins `identity()`. The | |
| launch id is minted once per interpreter (`types.process_fingerprint`). | |
| Host, pid, start time and Python version stay OUT of identity: a manifest | |
| must be re-readable on another machine without its id moving. | |
| Corollary, and it is a protocol constraint rather than a nicety: R, N and 0 | |
| are three separate forwards, so all arms of one comparison — and the | |
| replicates that make a delta — must be captured inside one process launch. | |
| A capture interrupted and resumed in a new process yields an arm 0 that is | |
| not comparable row-for-row to the R and N from the old one. | |
| `capture_plan` prints this in `protocol_notes` and stamps the current | |
| `process_launch_id`. | |
| **Why.** On this rig the H3 pipeline is bit-identical WITHIN a process and | |
| merely close ACROSS processes: per-process kernel autotuning puts a | |
| ~41-48 dB PSNR floor between two cold-process renders at the same seed | |
| (`.claude/skills/h3-latent-mechanics/SKILL.md` §7 in ComfyUI-ModelCatalog). | |
| Before this entry, `identity()` excluded tensor hashes while `Space.put` | |
| compared canonical JSON that includes `TensorRef.sha256`, so two honest | |
| reruns at one seed in two processes produced the SAME capture id and | |
| DIFFERENT bytes, and `put` accused the second of not being what its | |
| manifest said. The first legitimate replicate anyone captured would have | |
| hit it. | |
| **Alternatives.** A mandatory explicit `replicate` set by the caller — | |
| rejected: it defaults silently to 0, so the failure returns the first time | |
| someone forgets, which is exactly the population that needs the guard. A | |
| tolerance-based collision check ("close enough bytes are the same capture") | |
| — rejected: within a launch the pipeline is exact, so a tolerance hides a | |
| real defect in the case where the check is most informative. | |
| **Consequences.** `PROTOCOL_VERSION` bumps to `concept_lab/0.2.0` | |
| (DEC-CL-0006's rule: a field entering identity bumps the protocol, | |
| deliberately). DEC-CL-0007's rationale is amended below. T3 will refuse | |
| `alignment="exact_rows"` when `FunctionalDeltaManifest.same_process()` is | |
| False; the two `*_process_launch_id` fields are there for it now, as | |
| non-identity provenance. The cold-process floor gets MEASURED in E0 rather | |
| than assumed: the ~41-48 dB figure was taken on decoded renders, not on | |
| captured block states, and nobody has read the block-state number yet. | |
| --- | |
| ## DEC-CL-0007 · AMENDMENT (2026-08-15) · rationale narrowed, decision unchanged | |
| The refusal stands: same id with different bytes still raises. The reason | |
| it is correct is narrower than the original entry said. WITHIN one process | |
| launch the pipeline is bit-identical, so different bytes under one id mean | |
| one of the runs is not what its manifest says. ACROSS launches two honest | |
| reruns differ by kernel autotuning alone, and calling that a defect would | |
| have refused the first real replicate — which is why the launch id is now | |
| part of a capture's identity (DEC-CL-0015) and those two runs are two | |
| captures. | |
| `Space.put`'s message now says both halves: "Same id, different bytes. | |
| Within one process launch this means the id does not describe the | |
| condition; if these came from different launches the capture is missing its | |
| process fingerprint (DEC-CL-0015)." | |
| --- | |
| ## DEC-CL-0016 · ACCEPTED · 2026-08-15 · Corpora, tap specs and plans are definitions and want version control (amends DEC-CL-0008's rationale, not its default) | |
| **Decision.** The default index root does not move (DEC-CL-0008 stands, and | |
| it is the operator's call). But `Space.status()` reports | |
| `index_root_tracked` and warns when the index root is not inside a git work | |
| tree, and `corpus_new`, `corpus_add`, `corpus_status` and `capture_plan` | |
| carry the same warning. The recommendation is to point | |
| `H3_CONCEPT_INDEX_DIR` at a directory inside a PRIVATE repository, or to | |
| copy `corpora/` and `plans/` into one. | |
| **Why.** Manifests are not one kind of thing. Captures and deltas are | |
| RESULTS and can be re-measured from a corpus plus a stack. Corpora, tap | |
| specs and plans are DEFINITIONS: they encode which contrasts somebody chose | |
| to shoot, and no tensor reconstructs them. The ancestor program's F30 is | |
| the paid-for version of this: the definitions behind four findings lived | |
| only under a gitignored output directory | |
| (`docs/concept_lab/FABLE_SYNTHESIS_2026-08-15.md` §3.8). | |
| **Alternatives.** Moving the default index root into a repo — not taken | |
| here; DEC-CL-0008 settled the default deliberately and a public node pack | |
| is the wrong place for it to land by accident. Silence — rejected: the | |
| loss is invisible until the day someone wants to re-run the experiment. | |
| **Consequences.** Status and the corpus verbs warn. Whether the default | |
| moves is the operator's decision, and this entry does not pre-empt it. | |
| --- | |
| ## DEC-CL-0017 · ACCEPTED · 2026-08-15 · Split containment is enforced, and lineage counts as containment (closes DEC-CL-0014's open gap) | |
| **Decision.** `ConceptCorpus.containment()` lists every `content_hash` and | |
| every `source_id` that appears in more than one split, with the splits and | |
| row ids. `corpus_status` reports it and warns per collision; | |
| `capture_plan` REFUSES, naming the value and the splits, unless | |
| `allow_containment=True` is passed — and then the plan carries | |
| `containment_overridden` so the exception is in the record rather than in | |
| somebody's memory. `ConceptCorpusRow.source_id` is new and enters the row | |
| identity. | |
| **Why.** DEC-CL-0014 recorded this as a visible gap. A content hash alone | |
| does not close it: two crops of one take are different bytes and the same | |
| observation, so held-out means nothing if a lineage id is not checked too. | |
| Refusal rather than a warning at this one point, because plan time is the | |
| last moment when the fix is free. | |
| **Consequences.** A corpus written before this entry has no `source_id` | |
| anywhere, which reads as "not recorded" and never as "no collision". | |
| Absence is skipped, not treated as a shared value. | |
| --- | |
| ## DEC-CL-0018 · ACCEPTED · 2026-08-15 · The capture tap ships as two thin alpha nodes now (amends DEC-CL-0013's timing, not its rule) | |
| **Decision.** Capture must run INSIDE the ComfyUI process — the states live | |
| for microseconds inside the DiT block loop and the launch is part of the | |
| condition (DEC-CL-0015) — so the seam needs a node to exist. Two ship now, | |
| `MAIConceptCaptureArm` and `MAIConceptCaptureFlush`, under the category | |
| `MAI/concept (alpha)`. They hold no rules: the Arm node parses its strings | |
| into a `TapSpec`, reads a stack fingerprint off the live `ModelPatcher`, | |
| opens a `TapSession` and installs it on a CLONE; the Flush node calls | |
| `finalize()`. Every refusal comes from `concept_lab/`. | |
| DEC-CL-0013's rule stands and is what makes this safe: existing MAINodes | |
| behaviour and defaults do not move. The registration is the one-liner that | |
| entry predicted, in the same guarded loader tuple as `window_loop`, | |
| `window_expand` and `timeline.nodes`, so a broken alpha module cannot take | |
| the pack down with it. | |
| **Alternatives.** A monkeypatched sampler — rejected: `ModelPatcher`'s | |
| `add_wrapper_with_key` / `set_model_patch_replace` are the sanctioned seam, | |
| and the true-clock precedent is a monkeypatch we would rather not repeat. | |
| Waiting for T12 — rejected: nothing else in the subsystem can produce a | |
| capture, so waiting means the data layer keeps being tested against itself. | |
| **Consequences.** E0's gates are run THROUGH these nodes: a baseline render, | |
| a tap-installed-disabled render and a tap-enabled render at one seed in one | |
| process must decode to pixel-identical frames. If the pass-through gate | |
| fails, the nodes are pulled, not patched around. Until those gates run live, | |
| the nodes are alpha and say so in their DESCRIPTION. | |
| **Also decided here (mechanical, same change).** `TapSpec.store` becomes a | |
| comma-joined subset of `("stats", "frame_mean", "sketch", "full")` and is | |
| canonicalized to that order, because one recorded forward is expensive and | |
| the four reductions answer different questions off the same rows. Single | |
| words stay legal and keep their meaning, so no id already on disk moves. | |
| --- | |
| ## DEC-CL-0019 · ACCEPTED · 2026-08-15 · Capture identity is measured at the model boundary, not declared; the tap refuses before its first byte and streams into a partial dir | |
| **Decision.** Three parts, one rule. | |
| 1. A capture's `ConditionVariant` is built by the tap on its FIRST | |
| diffusion-model call, from what the model was handed: the text-encoder | |
| output (`sha256` of `context` through float32) as the first | |
| `ConditioningSource`, then one source per `minimax_payload["refs"]` block | |
| and one per `keyframes` block, hashed over their latents in packed order. | |
| `variant.name` stays a LABEL and stays out of identity; `variant.prompt` | |
| stays `None` (the human text is not reachable at this seam, and the | |
| encoder output the model consumed is the stronger identity). Noise-aug | |
| levels, `audio_scale`, the token-tag digest and the payload seed ride in | |
| the text source's `preprocess`, which `ConditioningSource.identity()` | |
| already covers, so no schema changed and `PROTOCOL_VERSION` does not move. | |
| 2. The payload seed must equal the seed the Arm node was given. The node | |
| cannot read the sampler's seed, so it is copied by hand; a mismatch now | |
| raises naming both numbers instead of mislabelling the capture. | |
| 3. `_ensure_capture_id()` refuses BEFORE the first write when the id is | |
| already on disk (manifest present, or a non-partial tensor directory), | |
| naming the existing variant, replicate, launch and paths. Tensors stream | |
| into `<tensor_dir>/<id>.partial/` while `TensorRef.path` records the final | |
| `<id>/...`; `finalize()` files the manifest through `api.capture_record` | |
| and only then renames the directory. `api.capture_exists()` is the stdlib | |
| door the tap knocks on. `Space.doctor()` reports leftover `*.partial` | |
| directories as `unfinished_capture` problems. | |
| **Why.** Both halves are the reported-vs-consumed family the ancestor | |
| program kept paying for, and both were caught live rather than reasoned | |
| about. `MAIConceptCaptureArm` built `ConditionVariant(name, arm)` with | |
| `sources=()` and `prompt=None`, and `ConditionVariant.identity()` is | |
| `{arm, sources, prompt}`, so every R cell hashed to one id and every N cell | |
| to another regardless of which plate the model actually saw: 12 of 15 E0.5 | |
| cells collided. Worse, the same-id refusal lived in `capture_record`, which | |
| runs at the END, while `h3_tap` had already streamed tensors into | |
| `<tensor_dir>/<id>/`. A collision therefore overwrote the earlier capture's | |
| tensors before anything detected it, and E0 replicate 0's tensors were | |
| destroyed that way. A refusal that fires after the damage is not a refusal. | |
| **Alternatives.** Putting `variant_name` into identity — rejected: it makes | |
| the label load-bearing, so a typo becomes a new condition and a copied node | |
| silently merges two. Declaring sources from the node's inputs — rejected: | |
| that is the reported number again, and the plate a node points at is not | |
| necessarily the latent the model got. Buffering tensors until finalize — | |
| rejected: a capture's worth of block states is exactly what T4 refuses to | |
| hold in VRAM. Deleting the partial dir on failure — rejected: it is the | |
| evidence that something failed. | |
| **How to apply.** Arms of one comparison differ by what they feed the model, | |
| not by what they are called; two arms with different plates now get | |
| different ids automatically, and two arms with the same plate and the same | |
| label get the same id and the second one is refused. An honest rerun of a | |
| condition inside one launch increments `replicate`. A `*.partial` directory | |
| in the tensor root is wreckage: `space doctor` lists it, the operator | |
| deletes it, nothing else touches it. | |
| **Consequences.** `tests/test_concept_lab_tap.py` grew sections 7-9 | |
| (identity from context and refs, refuse-before-writing and partial/promote, | |
| render fingerprint). The render fingerprint is evidence and not identity | |
| (DEC-CL-0006 stands): `MAIConceptCaptureFlush` hashes frame 0 and the whole | |
| decoded batch as uint8 into the manifest notes, so E0's pass-through gate is | |
| a hash comparison rather than two humans watching two videos. | |
| --- | |
| ## DEC-CL-0020 · ACCEPTED · 2026-08-15 · The first injection is additive at the tap, in frame_mean form, with its controls built in | |
| **Decision.** The first injection is additive at the tap, in `frame_mean` | |
| (and optionally separable frame + patch) form, on the target-video rows, | |
| with `zero` / `time_shuffle` / `sign_flip` controls built in; it is an | |
| instrument for E0.5's premise test, not the compiler. | |
| Mechanically: `MAIConceptInjectDelta` loads a delta pack (a `.safetensors` | |
| of `b<block>_s<step>/frame_mean` `[latent_t, hidden]` maps plus an optional | |
| `patch_mean` `[frame_rows, hidden]`, with a sidecar JSON carrying | |
| `latent_t`, `frame_rows`, `hidden`, the captured steps and the source | |
| capture ids) and, at the post-block point where `TapSession._record` reads, | |
| adds `alpha * d_frame[t]` to every row of latent frame `t` of the video | |
| segment, in place in the block's OWN output dict. `separable` adds | |
| `alpha * (d_frame[t] + d_patch[p] - grand)`, the rank-2 map with the shared | |
| grand mean counted once. `captured_only` fires on the pack's steps; | |
| `nearest_all` fires every step off the nearest captured entry, ties to the | |
| earlier one. Step index, layout stash and `cond_or_uncond` handling are the | |
| tap's, shared rather than copied (`h3_tap.schedule_and_sigma` / | |
| `step_from_sigma`, `TapSession._layout_signature`). | |
| **Why.** The capture side can be right and mean nothing: a delta that | |
| separates R from N in a bank is a statistic until something is generated | |
| with it. Additive-at-the-tap is the smallest thing that spends the delta | |
| where it was measured, so a null result indicts the delta rather than a | |
| translation layer. The controls ship WITH it rather than after it because | |
| the two questions the first read has to answer are both control questions: | |
| `zero` runs every line of the patch with zeroes (if that render moves, the | |
| plumbing is the effect, not the delta), and `time_shuffle` keeps the | |
| delta's energy while destroying its timing (if that renders like `none`, | |
| the timing content is not what is doing the work). A control invented after | |
| a promising render is a control chosen to survive it. | |
| **Alternatives.** Replacing rather than adding (set the rows' frame means to | |
| the pack's) — rejected for the first read: it destroys the run's own | |
| content and makes a null result unattributable. Injecting through the | |
| conditioning (a new ref block) — rejected: that is a different experiment | |
| about the packer, and it cannot address a chosen block or step. Waiting for | |
| a compiler that maps a factor to a schedule — rejected: the compiler's | |
| first input is exactly the measurement this node produces. A distinct patch | |
| NAME so a tap and an inject could share a block — rejected on the source: | |
| the block loop reads only `patches_replace["dit"]` | |
| (comfy/ldm/minimax/model.py:643-655), so a patch under any other name never | |
| fires, and comfy keeps ONE callable per (name, block, index) | |
| (model_patcher.py:93-112), so a second install on one block silently | |
| replaces the first. The injector therefore refuses on block overlap and | |
| says to put the tap at a later block, where it records the injected state. | |
| **How to apply.** One pack, one arm per render, all arms in one launch | |
| (DEC-CL-0015 binds injections too). Read `none` against `zero` before | |
| reading `none` against anything else. The geometry guard is | |
| (latent_t, frame_rows) recomputed off the live layout the way | |
| `TapSession._write` computes them, checked before any block fires, because | |
| the manifests carry segments and a position hash rather than the packed | |
| signature tuple; text_len and audio_t are free to differ, so a delta may be | |
| spent under a different prompt. `alpha` is the dose and it is not | |
| calibrated: the report logs the applied norms per (block, step) on the | |
| ComfyUI console at the last step, and that is the number to quote. | |
| **Consequences.** `concept_lab/backends/h3_inject.py` is the second and last | |
| file in the subsystem that imports torch; `types.py`, `space.py` and `api.py` | |
| stay stdlib-only. `h3_tap`'s step arithmetic moved to two module functions so | |
| both sides read one implementation; `TapSession` behaviour is unchanged and | |
| its suite is untouched and green. `tests/test_concept_lab_inject.py` is the | |
| new offline suite (pack reading, zero/frame/separable arithmetic, controls, | |
| step mapping, geometry refusals, cond_only, tap+inject on one clone). The | |
| node is ALPHA and has NOT been run live: E0.5's premise test is the first | |
| run, and if the `zero` arm is not bit-identical to an unpatched render the | |
| node gets pulled rather than patched around. | |
| --- | |
| ## DEC-CL-0021 · ACCEPTED · 2026-08-15 · Injection gains a full-state mode and a spatial-shuffle control | |
| **Decision.** `MAIConceptInjectDelta` gains `mode='full'`, which adds the | |
| whole stored block state (`b<block>_s<step>/full`, `[latent_t * frame_rows, | |
| hidden]` in the video segment's packed row order) row for row instead of a | |
| reduction, and `control='space_shuffle'`, which permutes the frame_rows axis | |
| with one seeded permutation shared by every frame. `frame_mean` injection | |
| was measured to act as a timing-blind global bias (E0.5, 2026-08-15), so the | |
| object injected must be the object measured to agree. | |
| **Why.** E0.5 spent a frame map and got a global push: | |
| `/mnt/work/ai/apps/ComfyUI-ModelCatalog/docs/concept_lab/E05_RESULTS_2026-08-15.md`. | |
| A frame map has already averaged the spatial axis away, so the arm has no | |
| "where" to be wrong about, and `time_shuffle` is the only control the object | |
| can even fail. The full state is the object the tap files and the object the | |
| cosines were computed on, and `space_shuffle` is the control that asks | |
| whether its placement is doing the work. A null on the reduction indicts the | |
| reduction, not the delta, and that ambiguity is what this closes. | |
| **Alternatives.** Reading the reduction harder (more blocks, more steps, | |
| bigger alpha) — rejected: it varies the dose of a quantity already known to | |
| be timing-blind. Holding the pack's full states in RAM the way the | |
| reductions are held — rejected: ~160 MB per (block, step) at production | |
| geometry. They are read one key at a time on first use and cached only in | |
| the model's own device and dtype, only for the (block, step) pairs a session | |
| selected, which is at most `blocks x steps` of them. | |
| **How to apply.** `space_shuffle` in `mode='frame'` REFUSES: frame_mean has | |
| no space axis and the arm would render identically to `none`, which is a | |
| control that cannot fail and therefore is not one. `mode='full'` refuses at | |
| session build if any selected (block, step) has no `full` tensor. Sidecar | |
| keys are unchanged; a pack may carry reductions, full states, or both, and | |
| the shape guard is `(latent_t, frame_rows, hidden)` as before. | |
| **Consequences.** `tests/test_concept_lab_inject.py` grows section 10 (full | |
| mode: exact add on video rows only, laziness, both shuffles as permutations | |
| of the axis they name, fp16 packs applied at the model's dtype); the pack | |
| reader now enumerates keys through `safe_open` rather than `load_file`, so a | |
| full pack's bytes are never all resident. The node is still ALPHA and the | |
| full mode has NOT been run live. | |