Confirmed on the rental: the previous version (phrase-scoped shift) showed
zero visible effect even at strength 1.5. Qwen's rows are contextual, so
every word in "a cat sits on a mat" already carries "sits", not just the
verb's own row -- editing 1-4 rows out of the sentence left the rest still
pointing at phraseA's action. Now shifts the whole prompt-token range toward
the alt sentence's own whole-prompt mean.
Multiple [a|b] spans in one prompt are additive: each span's delta is
measured against the original conditioning, not against a prior span's
already-shifted result, so they don't compound order-dependently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Default 0.5 shift left phraseA dominating the video. [phraseA|phraseB:N]
overrides strength per blend. Fixed a shared regex gap along the way: every
weight/strength markup in this file (not just blend) silently rejected
leading-dot decimals like ".8" -- fell through to literal text with no
error, reproducing the exact "override did nothing" bug this feature exists
to fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A real shortcut expanding to "wide shot: character walks" made [a|b]
never match at all, no error, because the previous fix banned ':' and
'@' anywhere in either phrase to stop one dangerous shape (an alt
phrase ending in ":1.5"). Two narrower shape-guards were tried and each
let a different shape through (colon-then-@, an em dash) while still
blocking ordinary prose. Reverted to fully permissive: a markup-shaped
alt phrase just reaches Qwen as literal punctuation in its own separate
re-encode, the same accepted trade-off this file already makes for a
weighted phrase's own stray parentheses -- a quality footnote, not a
leak, confirmed by tracing the encode path and its existing exception
handling.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial review of the previous commit found two real leaks and fixed
them: a colon in an alt phrase reached the real text encoder unstripped
(no bracket survives to re-catch it after substitution), and a wired
conditioning re-blended a second time because the re-tag guard only
checked for tag absence. Also reports per-span placement failures instead
of only counting successes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Measurement only, opt-in from Settings > Refinement & Taste. Reading map is free
(attention share on the phrase's keys per block); response map costs one extra
forward per phrase per model call. Phrases are numbered, never quoted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A latent unsharp on H3's 24-channel latent does not translate to picture
sharpness. Wrong mechanism, not wrong number; removed rather than tuned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The parser existed but nothing called it; the syntax went to Qwen as text.
One pass strips both markups so timed and weighted spans share one clean text;
both encode paths carry them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The editor strips the brackets before the encoding node sees them and hands the
windows to Studio as char spans on the text it gave that node.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Query rows are attended in chunks split at window edges, each with its own key
bias: exact, no SxS mask, no extra model call. Outside the window the phrase is
masked from video rows (H3-World single-egress); audio and text rows never see a
window. Composer lists the windows under the prompt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rate-this-render button lived at the far end of the bottom clip-editing
toolbar and required an explicit clip selection. Moved it next to Auto
Montage in the top action bar, and when the project has exactly one scene
it now resolves that scene automatically instead of requiring a click-select
first (with 2+ scenes, selection is still required -- no ambiguity to
resolve without one).
The rating block used to live in the timeline zone, whose fingerprint
already tracked selection/renders/ratings; moving it into the actionbar
zone needed those added to fpActionbar too, plus two more the button reads
indirectly: usesFunpackStudio() depends on project.conditioning_slot (not
previously tracked anywhere in this zone), and S.isGenerativeScene() can
flip on a video-clip conversion without the scene's rating changing.
Verified live (not just read) that toggling conditioning_slot off and back
onto "funpack" now correctly hides/reshows the button.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed on a real rental: the ~20s a REINS-injecting/block-repeat
generation now takes to start (no aimdo dynamic-VRAM streaming for that one
call) does not carry over to the next generation -- every other generation
this session keeps the compiler's normal speed. No behavior change; this
documents that the per-call scope (not a session-wide sticky disable, which
was drafted and reverted while chasing an initial guess about the 20s cause)
is the right one to keep.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
h3_explore_temperature was positive-only (flattens the softmax -- more
exploratory, pulled away from the prompt's own weighting). Same knob now
also takes negative values (widget min -0.9) to do the opposite: sharpen
attention so it commits harder to what the prompt already made confident,
instead of only ever being pulled away from it. Same code path (temperature
= 1 + strength, dividing Q before the dot product), sign decides direction.
Floored at 0.05 so an out-of-widget-range strength can't drive temperature
to zero or negative. Fixed a NaN gap the floor doesn't cover: `!= 0.0`
(needed so negative values install at all) is also true for NaN, and
max(1+nan, 0.05) is nan, not 0.05 -- excluded explicitly.
Editor UI widget (engine_settings.js) updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
aimdo's malloc-graph compiler assumes every block-scope forward allocates the
same pattern call to call. REINS while actually injecting (not passive
capture) and block repeat both patch what a block's forward does, which
crashed on a real rental with "aimdo memory compile error" -- confirmed via
instance log: identical workflow succeeds in passive-capture-only mode
(injection branch never entered) and fails the moment strength > 0. This is
a known upstream conflict class (Comfy-Org/comfy-aimdo #16144, #16342), not
specific to REINS; their fix is --disable-comfy-compiler, read live per
malloc-graph scope rather than cached at startup, so toggling it in Python
around just the affected sample_custom call works without touching the
(templated, rental-fixed) launch command.
Gated on actual install success (model identity before/after each
installer), not on the raw request flags -- a silently-refused install
(non-contiguous span, no block list, zero strength) leaves a block's
forward genuinely unpatched, so forcing the compiler off there would be a
pure, needless perf/VRAM regression instead of a fix.
Also: FUNPACK_AIMDO_LOG env var to raise comfy_aimdo's own native log level
to DEBUG, for diagnosing this class of failure directly from aimdo's side
when needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous caching test only exercised the zero-direction torch.zeros()
fallback -- round 1's actual bug lived in the real-direction branch
(direction.to(out.dtype).to(out.device)), which a torch.zeros count can't
see at all. Adds a second test using a deliberately non-matching dtype
(float64 direction against a float32 harness) so .to() is never a
coincidental no-op, then counts .to() calls on the direction tensor itself.
Verified by mutation: temporarily reverted dir_t_cache to round-1's buggy
shape (real-direction path uncached) and confirmed this new test fails
(3 calls instead of 1) while the old zero-direction-only test still passes
-- proving the old test alone would have let that exact regression through.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The caching logic itself (a block's second+ call reusing the built
direction/zero tensor instead of reallocating) had no regression test --
only the single-call math was covered. Counts torch.zeros calls across
three calls to the same block; only the first call's mask-cache-miss and
dir_t-cache-miss should allocate, the rest should be pure cache hits.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial review caught this: zero_dir_cache only cached the zero-vector
path. The real-steered block's direction.to(out.dtype) ran fresh every call
with no caching -- a genuine allocation every time, since direction is
always float32 (h3_repr_steering.capture()) while H3 runs bf16/fp16, so the
cast is never a no-op. From step 2 of a scene onward that put the steered
block back to allocating one more tensor per call than the other 49 --
exactly the per-call allocation-count asymmetry the uniform-clone commit
was meant to close, just narrowed rather than eliminated.
Renamed zero_dir_cache -> dir_t_cache and cache the real cast-direction
result the same way the zero fallback already was: built once per
(dtype, device) combo, reused every later call for that block.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real crash report from the rental, not speculation this time: "aimdo memory
compile error" inside comfy_aimdo's malloc_graph (comfy/model_prefetch.py's
prefetch_queue_pop(..., malloc_scope="block")), dying before the first
denoising step completes. That scope string is passed identically for every
one of H3's 50 blocks within a single forward pass -- aimdo records one
allocation sequence across all 50 block-scope entries per pass and breaks
when that sequence isn't uniform block to block.
REINS's hook only ever cloned+indexed+injected on the block(s) actually
being steered (direction is not None), typically 1 of 50 -- the other 49
did nothing extra. That's a block-to-block allocation-shape mismatch within
a single pass, present from the very first forward call, which matches
"crashes before step 1" better than the cross-step timing race this file's
last several commits were fixing.
Now every candidate block runs the identical clone -> index -> torch.where
sequence whenever REINS is steering anywhere, using a zero direction vector
for blocks with no learned direction (or not enough rated data) so the math
is a genuine no-op (bump=0) while the allocation shape stays uniform. Real,
accepted cost: up to 50 full clones of the block's hidden state per step
instead of ~1, whenever REINS is active -- the user explicitly chose this
over cheaper, less certain options after the first several attempts (host
sync removal, unconditional single-block clone) didn't fix the actual
crash.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 3 of adversarial review, executed and confirmed live: the "mask is
not None implies non-empty" reasoning behind has_rows=True was false.
_mask_from_mod_segments' scalar branch sets found=True without checking the
segment has nonzero length, so a zero-length scalar-tagged segment produces
an all-False mask that is not None -- has_rows=True would then make
capture() silently return an all-NaN descriptor instead of None, poisoning
that block's learned direction() on disk with no error anywhere. No
reachable path into a zero-length segment was found in this repo's own code
(the real video segment is never zero-length; keyframe/ref segments come
from real encoded latents here), so this doesn't fire today, but the
invariant it depended on isn't enforced or asserted anywhere, so a future
upstream ComfyUI change could trip it silently.
Fixed the right way, which was available at the same cost av_decouple
already pays (h3_repr_steering.capture()'s has_rows param exists for
exactly this): cache a REAL bool(mask.any()) once per unique seq_len,
alongside the mask itself, in both h3_repr_steering's hook and
h3_q_steering's override -- same single-sync-per-generation cost class as
av_decouple's has_v/has_a/has_other, zero per-block/per-step syncs, and
correct regardless of what video_mask_from_mod_segments actually guarantees.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 2 of adversarial review: my earlier "fix" still called bool(mask.any())
inside h3_repr_steering.capture() and h3_q_steering's _override on every one
of the 50 blocks, every step -- despite the mask itself being cached per
seq_len, the .any() reduction was recomputed and host-synced every call. The
av_decouple mechanism already had the right pattern (cache .any() once per
seq_len) and its own comment falsely claimed capture() had been fixed the
same way; it hadn't.
Both real callers (h3_repr_steering's hook, h3_q_steering's override) build
their mask via video_mask_from_mod_segments, which by construction only ever
returns non-None when it found at least one True row -- so both can pass
has_rows=True and skip capture()'s internal check entirely, with zero syncs
in the hot path. capture()'s own default (has_rows=None) still self-checks
for any other/future/test caller that can't make that guarantee.
Also dropped the .cpu() added in the first commit: a 50-descriptor-per-step
[hidden_size] vector is a few hundred KB, not a real VRAM pressure source,
and forcing that transfer synchronously was itself a sync in the exact loop
this whole fix exists to keep sync-free. Reverted to leaving it on GPU
(original behavior) -- torch.save/load already handles device-tagged tensors
fine downstream.
q_steering's unconditional q.clone() was flagged this round as likely pure
waste under ComfyUI's single-owner attention-tensor-container contract, but
left in place: verifying that contract myself with confidence is out of
scope here, and the cost of being wrong (reintroducing the aliasing bug just
fixed for h3_repr_steering's out) outweighs the small clone it would save.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial review round 1 caught this: out is not provably this hook's own
private tensor. A chained h3_block_repeat span_loop passthrough returns
args["img"] verbatim with no copy for non-head blocks in its span, and
h3_repr_steering installs as the OUTER wrapper around it (_install_block_
influence then _install_h3_repr_steering in _sample_chunk) -- so `out` here
can be the H3 model loop's own live `h`. Removing the clone entirely (as the
previous commit did) reintroduced exactly the aliasing hazard block_repeat's
own _repeat() explicitly guards against one function away. Restored the
clone, kept unconditional (not gated behind the safe/unsafe branch) so it
still doesn't need the host sync this fix exists to remove.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
comfy_aimdo's dynamic-VRAM block prefetch pipelines weight cast/unpin against
CUDA stream timing; a CPU sync (bool()/item() of a CUDA tensor) or a surprise
allocation mid-loop can race it into reclaiming a weight buffer a still-queued
kernel needs -- a silent segfault, no Python exception possible. All three H3
rating-driven dit hooks did this on every block, every step:
- h3_repr_steering's injection guard and h3_q_steering's injection guard both
did `if torch.isfinite(x) and x > 0:` (forces a host sync) before writing
the steered rows, and h3_repr_steering cloned the whole block output first.
Replaced with a GPU-only torch.where guard; dropped the now-unnecessary
clone in h3_repr_steering (out is this hook's own private tensor already).
- h3_repr_steering.capture() now .cpu()s its descriptor immediately instead
of leaving it on GPU competing for VRAM headroom for the rest of the run.
- h3_av_decouple's attention override re-checked `.any()` on its video/audio/
other masks every single attention call; now decided once per unique
seq_len alongside the mask itself (typically once per generation).
Unvalidated against a real GPU (none available here) -- worth confirming on
the rental that REINS no longer crashes silently under dynamic VRAM offload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The direct "ref:<id>" picker for a widget-level reference (VHS_LoadVideo's
"video", etc.) forced reselecting the field by hand every time the
underlying reference file changed. Adds the same numbered indirection
Input Sources already gives sockets: "Reference video 1" is whatever is
marked first among video references, not a specific file, so re-marking
in the Media Bin re-points the widget with no node-config change.
Adversarial review of this addition, run to convergence, found and fixed
in order:
- a real installed file whose name collided with the sentinel pattern
("ref#video:1", "ref:whatever") could be silently hijacked instead of
used as-is -- fixed by having a literal installed choice always win,
identically in the builder and the picker;
- the self-heal fallback for a stale plain value could reach into
reference-slot space and silently attach a live Media Bin dependency
the user never picked;
- fixing that reopened the underlying onChange(undefined) hole the
fallback chain had been papering over, wiping a stale plain value
outright when nothing was installed -- restored the original
choices-only self-heal invariant explicitly;
- a reference-capable widget could be added to a Linked Inputs group,
whose own value editor builds a spec with no ref_kind -- silently
wiping the shared reference across every member on next render.
Simplest safe fix: that kind of field doesn't link.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A reference's file lives in ComfyUI's own input folder (so the node can
load it), which means it was also showing up a second time in the
combo's plain choice list under its on-disk name
(funpack_movie_<id>.mp4) -- indistinguishable from any other reference,
right next to the friendly "R1 - name" entry for the same file. Drop
the raw duplicate; the friendly one is always the better pick.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial review of the previous commit found two real gaps in the
"reference into a file-upload widget" feature: unmarking a wired
reference in the Media Bin silently rewrote and persisted a plain
default over it on the node panel's next render (the exact silent
substitution the feature was meant to avoid, just via a path the
builder's own report never sees), and a wrong-kind reference reported
the same "no longer in the media bin" message as a genuinely missing
one, sending the user to check the wrong thing.
A second pass on the fix itself found the new "is this a reference"
check wasn't scoped to reference-capable fields, so an ordinary combo
with an installed file literally named "ref:..." would misreport.
Scoped it to spec.ref_kind.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
VHS_LoadVideo's "video" field (and LoadImage/LoadAudio/LoadVideo's own
file fields) are plain combo widgets, not wireable sockets, so a marked
reference video had no way in except a manual upload or path -- which
meant no way to force VHS_LoadVideo's own framerate/frame-cap controls
onto a reference clip without re-uploading it outside the Editor.
The node panel's combo fields for these known upload widgets now offer
the marked Media Bin references of the matching kind, using the same
"ref:<id>" values as Input sources. The builder resolves them to the
reference's filename at build time; an unresolvable or wrong-kind
reference is reported instead of silently defaulting.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The panel rebuilds its whole DOM on any relevant store change,
including marking/unmarking a reference -- which only actually
changes one card's badge. That recreated every <img>/<video> thumb
from scratch each time, so every preview visibly reloaded and every
video redecoded its first frame for an unrelated single-item toggle.
Cache the built preview node per media id and re-append (move, not
recreate) it across rebuilds instead. Verified live: toggling a
reference mark on/off now leaves the same DOM node instances in
place. Adversarially reviewed; holds up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
For nodes with parallel image/video autogrow reference lists (MiniMax
H3's ref_images/ref_videos and anything following the same naming
convention): when both end up filled at the same index, only the
video is sent -- letting you switch between image-as-reference and
video-as-reference runs without manually clearing the one you're not
using. Per-node opt-out toggle, on by default.
Adversarial review (three rounds) found and fixed a real gap: dropping
an image reference from the middle of a multi-reference list left a
non-contiguous index sequence, which the V3 autogrow expansion this
project already relies on cannot parse. Surviving entries are now
re-indexed to close the gap.
Also fixes an unrelated bug the same review surfaced in the Linked
Inputs picker: clicking "Save link"/"Add" while an unrelated node-page
edit sat buffered in the deferSave queue silently persisted that edit
too, bypassing the normal unsaved-changes confirm. It now runs through
the same confirm/discard path as leaving the page any other way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial review of the previous commit found two ways an
incompatible node input could silently corrupt an existing linked
input's shared value: no kind check at all, and (after fixing that) a
same-kind combo with a different choices list still slipping through
and getting overwritten via the widget's own stale-value self-heal,
which then broadcast to every other member of the link.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Models > Linked inputs could only create a new link; there was no way
to add more node inputs to one that already existed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 2 of review confirmed the class-scoping fix closes the cross-wire
bug with no new defect, but flagged that the new test only exercised
two DIFFERENT loader classes -- it couldn't have caught an over-fix
into a unique node per socket, losing the original one-loader
shared-by-multiple-sockets design the class-scoped id is supposed to
preserve. Added the missing case.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial review found two real bugs in the new prevvideo feature:
1. The loader node id was a bare "prevvideo_load", copied from
prevframe's pattern -- but unlike prevframe (always LoadImage),
prevvideo's loader class varies per destination socket type
(LoadVideo vs VHS_LoadVideo). Two prevvideo sockets needing
different loaders in the same run would share one node via
graph.setdefault, silently wiring a mismatched-type output into
the second socket while the builder reported both as cleanly
wired. Fixed by scoping the id to the loader class, matching the
sibling ref#/ref: bin-reference path's own ref_load_{id}_{cls}
pattern -- prevvideo should have followed that one, not
prevframe's single-class shortcut.
2. store.js's declared-failure notice used .find() across
report.unsatisfied, so a run where BOTH a prevframe socket and a
prevvideo socket failed to retrieve the predecessor's render would
silently drop one of the two messages. Now collects every match.
Added a two-socket regression test proving the loader-scoping fix
(distinct node ids, correct class per socket).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same idea as "prevframe" but passing the entire previous scene's
rendered clip through instead of extracting one frame from it: a new
"prevvideo" input source wires whatever LoadVideo-family node the
destination socket needs, pointed straight at the predecessor's own
TEMP file via ComfyUI's "[temp]" annotation -- no ffmpeg, no copy, no
media-bin entry.
Refactored the shared "resolve and sanitize prev_scene_media into a
safe TEMP path" logic out of _extract_prev_scene_frame into
_resolve_prev_scene_media so both paths share one place that trusts
none of the client-supplied filename/subfolder as literal paths.
Reuses the R2V pipeline's own _reference_loader/_widget_defaults to
pick LoadVideo vs VHS_LoadVideo by what the destination socket wants,
same as the existing "ref#"/"ref:" sources. Missing predecessor stays
silent; a predecessor that rendered but can't be retrieved is declared
through the same report.unsatisfied -> state.notice path as prevframe.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 3 found the extraction's OUTPUT filename was built from the
requesting scene id with no sanitization at all -- unlike the input
side, which round 2 already locked down. A scene_ids-driven request
(bypassing _solo()'s only_scene validation) or a project imported with
a crafted Scene.id could write the extracted frame outside ComfyUI's
TEMP directory. Fixed at the sink: scene_id is now stripped to a safe
character set before it ever reaches a path, regardless of where it
came from, plus a scene_scope membership check as defense in depth.
Also found: the "declared, not silent" warning for a failed extraction
(added in round 1) was returned by the API in report.unsatisfied, but
store.js never read that field -- it reached no one. Now surfaced
through the same state.notice path as the existing "ignored settings"
warning, concatenated rather than overwriting it when both fire in the
same run (round 4 caught the two clobbering each other).
Added a regression test proving the old code would have written outside
tempdir with this exact payload, and that the fix keeps it contained.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Second adversarial pass on the previous fix found two real regressions
it introduced: a non-dict prev_scene_media (malformed client JSON) threw
AttributeError inside the request's try block, failing the ENTIRE
generate call instead of gracefully leaving the slot unconnected; and
the extraction filename was scoped to target.scenes[0].id, which is
the first scene of whatever generative unit _solo() expanded the
request into — not the scene actually being generated — so two
concurrent solo requests for different scenes in the same unit would
clobber each other's extracted frame.
Also swapped the hand-rolled tempdir-containment check for
folder_paths.is_within_directory, which the codebase already has and
resolves symlinks before comparing (the hand-rolled version did not).
Added direct tests for _extract_prev_scene_frame itself (malformed
input, traversal, missing ffmpeg, scene-scoped naming) — round 1's
fix had none, only builder-level tests of its output.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial review of the previous commit found: the extracted frame was
written into ComfyUI's global input/ folder and never cleaned up (an
actual visible asset, contradicting the point of not touching the media
bin); the client-supplied filename/subfolder round-trip through the saved
project JSON unsanitized, allowing path traversal into ffmpeg's input;
and a missing/garbage-collected predecessor render failed identically
to "scene 1", giving the user no way to tell the two apart.
Now extracts into ComfyUI's TEMP folder (loaded via LoadImage's "[temp]"
annotation — wiped on restart like any other preview, never input/ or
the bin), sanitizes filename/subfolder against traversal, and reports a
distinct "could not be retrieved" warning when a predecessor existed but
its render is gone. Output filename is scene-scoped instead of a
process-random hash, closing the accumulation and cross-request-race
angles as a side effect. Added builder tests for all three prevframe
outcomes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New "prevframe" input source: server extracts the preceding scene's
last rendered frame via ffmpeg into ComfyUI's input folder (never the
media bin) and the builder wires it through a plain LoadImage. Scene 1
or a solo re-run with nothing rendered before it leaves the socket
unconnected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial review of the leak fix found the new tests only exercised REINS
stacked on itself, leaving 6 of 7 install mechanisms and the entire
optimized_attention_override chain (av_decouple/attn_temperature/q_steering)
untested. Add a test stacking REINS -> av_decouple -> q_steering -- different
mechanisms chaining onto each other, the realistic multi-run leak shape -- and
confirm one strip call unwinds both the per-block dit dict and the single-slot
attention override chain.
patches_replace["dit"] and optimized_attention_override installs had no tag+strip
protection, unlike the scene-wrapper and block-hook mechanisms fixed for the same bug
class earlier. An interrupted run left these hooks on the shared model; the next run
chained a new layer onto the leaked one instead of starting clean, growing unbounded
across interrupted runs -- the likely cause of the live "potential memory leak...
circular references" warnings and slow full-GC pauses.
REINS' equivalent strength dial caps at 2.0, but Q-steering only reaches the
output through softmax rather than a direct hidden-state addition, and live
testing this session found strength 1.0 barely moved the output on a small
block range. Raised well past REINS' number to give room to search before
concluding the mechanism itself is weak.