Skip to content

feat(webv2): Ref2VA reference-extend — append the generation to an initial video - #191

Merged
lstein merged 20 commits into
mainfrom
feat/minimax-h3-ref2v-extend
Sep 1, 2026
Merged

feat(webv2): Ref2VA reference-extend — append the generation to an initial video#191
lstein merged 20 commits into
mainfrom
feat/minimax-h3-ref2v-extend

Conversation

@lstein

@lstein lstein commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #190. Adds the Ref2VA counterpart of FL2VA's extend mode: an Initial Video section on the reference panel whose clip the generated video is appended to, with continuity supplied by a linked tail reference instead of frame conditioning.

The flow

  1. Upload / drag a video into Initial Video (now shown in reference mode).
  2. Pick the Start Frame / End Frame cutpoints.
  3. A linked video reference appears at position 1 — badged "Initial video", counted against the 3-video cap (e.g. 1/3), conditioning defaulting to Video + audio.
  4. Its trim defaults to the last 141 frames (~5 s at 24 fps) before the End Frame[end − 140, end] — and re-derives live whenever the cutpoints move. It is otherwise an ordinary card: re-trimmable (the "frames to sample" knob), reorderable, removable, conditioning-selectable.

141 sits exactly on H3's 17n+5 frame grid, so the backend's reference snap-down keeps the whole window.

Mechanics

  • Graph: same trimmed-extract + crossfade video_concat join as FL2VA extend (addSourceJoin, factored out of the extend scaffolding; no last-frame extraction, no frame conditioning). video_concat rebuilds the soundtrack from every input, so both clips' audio crosses the seam. No backend changes. generation_mode stays minimax_h3_ref2v, with the extend-style source_video + trim extras riding along for recall.
  • State: references and sourceVideo now coexist; validation rejects the pair on models without reference-extend, and references stay exclusive with first/last frames. The linked reference carries a panel-only fromSourceVideo flag (never recorded in metadata).
  • Transitions: FL2VA → Ref2VA carries an initial video over and derives its tail reference; switching back clears references and keeps the clip for FL2VA extend.
  • Recall restores references and the source video (with its recorded trim) together; the linkage re-establishes by clip identity on the next cutpoint edit.

Adversarial review

A fresh-context review ran before push; confirmed findings fixed in the second commit with regression tests:

  • the gallery optimistic-rollback rules still treated sourceVideo ⊕ references as rivals, so a failed gallery deletion permanently dropped one of the pair;
  • the linked-entry matcher could rewrite a user's own same-clip reference (hand trim and all) on a source swap, leaving two flagged entries;
  • task-neutral transitions (same-model re-selection) reset hand-tuned linked trims;
  • a full 3/3 video cap silently produced an extension with no continuity reference — now a warning toast.

Testing

6765 webv2 tests pass, including new coverage: trim derivation/clamping, linkage add/update/adopt/remove/cap, flag-priority matching, mixed validation, FL2VA↔Ref2VA transitions, the reference-extend graph shape (concat output, no frame conditioning, metadata payload), recall of the combined recording, and the rollback-restore regression.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A

lstein and others added 2 commits August 30, 2026 21:03
…itial video

The Ref2VA panel gains an Initial Video section: the generated clip is
appended to the uploaded video at its End Frame cutpoint (same
extract + crossfade video_concat join as FL2VA extend; video_concat rebuilds
the soundtrack from every input, so both clips' audio survives). Continuity
comes from a linked video reference the panel derives from the clip — its
trim defaults to the last 141 frames (~5s at 24 fps, exactly on the 17n+5
grid) before the cutpoint and re-derives whenever the cutpoints change,
while remaining an ordinary reference card: trimmable, reorderable,
conditioning-selectable, counted against the 3-video cap, and badged as
linked. No frame conditioning and no backend changes; generation_mode stays
minimax_h3_ref2v with the extend-style source_video extras riding along.

State-wise, references and sourceVideo now coexist (validation still
rejects the pair on models without reference-extend, and references remain
exclusive with first/last frames). Switching FL2VA -> Ref2VA carries an
initial video over and derives its tail reference; switching back clears
references and keeps the clip for FL2VA extend. Recall restores both the
references and the source video with its recorded trim, and the linkage
re-establishes by clip identity when the recalled trims are next edited.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A
- Drop the sourceVideo <-> references rival rows from the gallery
  optimistic-rollback rules: the pair is legal on a reference-extend panel,
  and treating them as rivals made a FAILED gallery deletion permanently
  drop whichever slot the sweep had cleared.
- applyReferenceExtendSourceVideo now prefers the flagged linked entry over
  an earlier same-name reference: a flag-or-name findIndex rewrote a user's
  own hand-trimmed reference (and left a second flagged entry dangling)
  when it sat above the linked one during a source swap.
- The model-selection transition derives the linked tail reference only
  when none exists yet — it also runs on task-neutral edits (same-model
  re-selection, component changes), which reset hand-tuned trims the help
  text only promises to reset on cutpoint changes.
- Setting an Initial Video with all 3 video-reference slots occupied now
  raises a warning toast instead of silently generating an extension with
  no continuity reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A
lstein and others added 17 commits August 30, 2026 21:22
…me rate

The 141-frame default was source-native, so a 16 fps clip sampled ~8.8 s
instead of the intended ~5 s. Treat 141-at-24fps as a duration and convert
it to source frames (16 fps -> 94, 30 fps -> 176); 24 fps sources keep
exactly 141, which stays grid-exact through the backend's 24 fps resample
and 17n+5 snap-down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A
… count

The parent scaled the tail window to the source frame rate, fixing the ~8.8s
a 16 fps clip sampled. The window is still bounded by a second rule it does
not account for, and that one costs the frames continuity actually depends on.

Three backend rules bound a video reference, and every one of them discards
from the END -- the frames adjacent to the cutpoint. normalize_reference_video_frames
resamples onto H3's fixed 24 fps and truncates to the GENERATED frame count
keeping the FRONT (frames[:num_frames]); encode_reference_video then snaps what
survives DOWN to the 17n+5 grid. So a 141-frame window against the 124-frame
default keeps 124 frames -- the right count, shifted 17 frames (0.7s) off the
seam. The window is now min(TAIL, numFrames), the smaller bound winning.

The fps conversion rounds UP rather than to nearest. Overshooting is free: the
truncation cuts the window back to exactly numFrames and the snap -- both
already on the 17n+5 grid -- then keeps it whole. Undershooting lands OFF the
grid and the snap eats up to 16 frames. Rounding to nearest lost 17 frames at
23.976 fps (NTSC film) at every frame count on offer, and 17 at 16 fps at the
124-frame default. A test runs all three backend rules over 12 source rates x
all 16 frame choices and asserts the kept count equals the budget exactly.

Two defects found by adversarial review of the first cut, both created by
making the window depend on the frame count:

- The frame count is snapped BEFORE the window is derived. Deriving first let
  a Wan panel's count (as low as 5) size the window for a 90-frame H3
  generation, under the 13 frames sample_text_conditioning_frames needs --
  Generate failed outright with a ValueError.
- The frame-count application re-derives instead of shrinking to fit. The
  Frames number input emits a value per keystroke, unclamped, so typing "345"
  arrives as 3 -- and a shrink-only rule pinned the window at a 3-frame budget
  for the rest of the session, silently. Re-deriving is idempotent, so
  keystrokes and slider drags leave no trace.

Separately, the linked tail reference is APPENDED rather than prepended.
Request order is rotary order: build_ref2va_packed_sequence lays the reference
blocks out in order, each advancing a shared clock, and the generated rows
start at the position the LAST block left behind. The reference the model
continues from is the final one, so the continuity anchor belongs at the end --
with a single reference this changed nothing, which is why it went unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
`toTailAwareIndex` sends a near-the-end bound out as a NEGATIVE index the
backend resolves against the clip's REAL frame count, and leaves anything
further back POSITIVE, taken from the panel's ESTIMATE (duration x fps). That
split is right for a hand-picked trim, whose start is an absolute position the
estimate must not drift.

The linked tail window straddles it: the cutpoint end goes negative while the
start, a tail-length back, stays positive -- so what the backend extracts is
`tail + (real - estimate)` frames, not `tail`. The length is a budget the
backend enforces by discarding the overrun at the SEAM, so an inexact estimate
put back exactly the frames the budget exists to protect. It also meant an
estimate that overshot by more than `tail - 1` walked the start past the end
and raised, a margin that shrank with the frame-rate-scaled window (140 frames
flat before, as low as 29 for an 8 fps source).

The linked entry's start is not an absolute pick -- it is defined as `tail - 1`
frames before the cutpoint -- so it now rides the same negative anchor and the
window keeps its exact length at any real count. Two cases stay absolute: a
start the clip's own beginning already clamped to 0, and a cutpoint far enough
from the end that both bounds already share the estimate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
Request order is rotary order: build_ref2va_packed_sequence lays the reference
blocks out in order, each advancing a shared clock, and the generated rows
start at the position the LAST block left behind. The linked tail reference
only anchors anything if it IS that block -- an image dropped after the Initial
Video wedges a whole rotary slot between the initial video's tail and the first
generated frame, and the model continues from the image instead. Two ordinary
actions in the wrong order were enough, with nothing in the UI to say so.

The anchor's position is now derived like its trim: last, always.
`pinReferenceExtendAnchor` runs in the reference field's single setter, so it
covers add, remove, retrim and drag alike, and on the adopt path where a
recalled entry is claimed in place.

The video cap is enforced at both doors rather than only one. The add-video
button was already gated; the Initial Video field now disables itself when the
video slots are full and no anchor exists yet, since setting a clip there has
to place one. With an anchor already present the drop only re-derives it in
place, so it stays available -- swapping the initial video keeps working.

And `sanitizeVideoReferences` overflows videos from the FRONT. It keeps the
first N of each kind, which for videos is now exactly backwards: with the
anchor pinned last, tail-dropping discards the one entry the extension depends
on. An over-cap list should no longer reach it from the UI at all, so this is
the guard for a stale or hand-edited project record. Images have no ordering
role and keep the front.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
Clicking the randomize-seed toggle in the Video panel moved focus to the Seed
text field and never toggled anything. The Upscale panel had the same bug.

Chakra's `Field.Root` hands its single `ids.control` to EVERY control nested
inside it, and both panels' seed rows put three controls in one `Field` -- the
seed NumberInput, the shuffle button and the switch. The switch's hidden input
therefore claimed the same id as the seed input:

    seedId=_r_0_  swId=_r_0_  collision=true
    switchLabelFor=_r_0_      focusedSeed=true  fired=0

`Switch.Root` renders a `<label>`, and `label[for]` resolves to the FIRST
element carrying that id -- the seed NumberInput, earlier in the DOM. So the
click activated the seed field and the switch never saw it. Giving the switch
its own `ids.hiddenInput`, derived from `useId` so it stays unique across
widget instances, breaks the collision.

The Video panel's label text was also an inert `<Text>` sibling outside
`Switch.Root`, so even a working switch would not have responded to a click on
the words. It is now a `Switch.Label`, matching Upscale.

Covered by a browser test asserting the switch and the seed input hold
different ids, that the switch's own label points at the switch, and that
clicking either the control or the label toggles without focusing the seed
field. Verified failing before the fix (`fired=0`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
… slop

The relative start index resolves to `startFrame + (real - estimate)`, which
goes NEGATIVE once the estimate overshoots by more than `startFrame` --
and `_ResolvedVideoRange.resolve` rejects an out-of-range index outright
rather than clamping, so Generate failed with

    start_frame=-142 is out of range for a 138-frame reference video

The guard was `startFrame === 0`, which misses by exactly the margin that
matters: a start of 1 is the common case for a clip barely longer than the
window, and `duration x fps` overshooting by a frame or two is the error
`toTailAwareIndex` was written to absorb in the first place. Before the
relative start existed the same input emitted a positive 1 and generated fine,
so this turned a small quality loss into a hard failure.

The guard is now `startFrame <= TAIL_INDEX_SLOP`, reusing (and naming) the
3-frame allowance `toTailAwareIndex` already applies at the other end of the
clip. Below the slop the window fills nearly the whole clip, where its length
cannot be honoured anyway, and the absolute form is always in range; the drift
it costs there is the estimate error itself. Above it the window keeps its
exact length at any real count, which is what the relative form is for.

Verified against the review's failing cases: est=143/real=141, est=145/real=138,
est=92/real=90 and est=1002/real=1000 all resolve, while est=402 keeps a
141-frame window at real counts of both 400 and 396.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…e cap trim

Two of this branch's own changes combined into data loss.

`sanitizeVideoReferences` was changed to drop videos from the FRONT, on the
reasoning that the anchor is pinned last so a tail-drop would discard it. That
holds only for panels this build wrote. The commit immediately before this
branch PREPENDED the anchor, so every project saved before it loads with the
anchor at index 0 -- and the front-drop then deletes exactly the entry it was
added to protect.

The second half is quieter and hits every already-saved panel, over the cap or
not: normalization never pinned, so a prepended anchor stayed first. The
generated frames continue from the LAST reference block, so those panels keep
generating from whatever reference follows the anchor. Nothing repaired it --
`setNumFrames` does not pin, and the model-selection transition skips the
derive entirely when a flagged entry already exists -- so it persisted until
the user happened to touch the reference list.

Pinning inside `sanitizeVideoReferences`, ahead of the cap trim, closes both:
the anchor is last by the time the overflow rule runs, so the front-drop
protects it, and normalization runs on every read, so old panels heal on load.

The pins in `setReferences` and `applyReferenceExtendSourceVideo` stay. They
are redundant with this one on any path that normalizes, but they keep the
written value correct for any that does not, and a violated ordering invariant
is silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
`addImageReference` / `addVideoReference` closed over `references`, awaited a
gallery resolve, then wrote `[...references, item]` -- the array as it was at
drop time. `isLoading` gates the reference cards and the drop zone, but not the
Initial Video field or the Frames slider, and this branch made both of those
writers of `references`. Two losses, neither of them warned about:

- Drop a video into References; while it resolves, set an Initial Video. The
  resolve lands and writes the pre-anchor array. You get `sourceVideo` set with
  NO anchor, the cap gate reading false and validation passing, so the
  extension generates with no continuity reference at all.
- Drop an image into References; while it resolves, drag Frames 345 -> 90.
  `setNumFrames` re-derives the anchor to a 90-frame window; the resolve then
  restores the stale 141-frame one. `frames[:90]` keeps the FRONT, so 51 frames
  at the cutpoint are discarded -- the exact bug this branch exists to fix --
  and nothing re-derives afterwards, so it is permanent.

`onChange` now takes an updater instead of a snapshot, and `setReferences`
applies it against a ref holding the newest normalized list, so a write that
landed during the await is merged rather than clobbered. Every handler
converted, not just the async ones: a delete or reorder during an in-flight
resolve is the same race.

The ref syncs in an effect rather than during render -- the react-compiler rule
forbids touching a ref while rendering, and a gallery resolve lands whole
frames after the commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
The gate was `no flagged entry && videoCount >= max`, but the setter refuses
only when `no flagged && no same-name entry && videoCount >= max`. Adopting an
existing reference for the clip consumes no slot, and the gate ignored that
term -- so it disabled the field in a state the setter handles fine.

Metadata recall lands exactly there. `graph.ts` records the references without
`fromSourceVideo` (it is panel state, deliberately not metadata) and records
`source_video` separately, so recalling a reference-extend generation that used
three video references restores three UNFLAGGED videos beside the clip: no
flagged entry, three videos, gate fires.

`disabled` reaches the clip's trim sliders as well as the drop target, so the
cutpoint could not be moved at all on a clip that was legitimately set,
re-dropping the same clip to re-establish the linkage was unreachable, and the
message ("Remove a video reference first") was wrong -- there was room.

Both sides now defer to `canPlaceReferenceExtendAnchor`, and a test drives the
predicate and the setter over the same seven reference states and asserts they
agree, so the gate cannot drift from the behaviour it describes.

The gate stays conservative for a clip not yet dropped: its name is unknowable
before the drop, so the answer is whether a NEW anchor would fit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
… grid exactly

Two seam losses in the same function.

A clip shorter than the tail window fell through to `Math.max(0, ...)` and the
window became the whole clip -- an arbitrary length, so generally OFF the
17n+5 grid, and `encode_reference_video` snaps DOWN and drops the difference
from the END. No estimate error needed: at 24 fps a 120-frame clip with
numFrames 345 kept 107 of its 119 frames and stopped half a second short of the
cutpoint. It was the common case for any source under ~5.9s at 24 fps, and the
grid test could not see it because it only ran a 402-frame clip. The clamped
branch now falls back to the largest ON-GRID budget the clip does support, so
the same frames land ON the seam instead of before it.

And `ceil(budget * fps / 24)` was never exact. The resample is a step function,
so rounding up overshoots by a frame or two, and the overshoot is discarded at
the seam. Walking down to the smallest source count that still reaches the
budget hits it exactly wherever the source's frame boundaries allow, and within
one frame where they cannot (12 fps cannot reach an odd 141 at all). Over 15
rates x all 16 frame choices the total seam offset halves, 72 frames to 36,
with no budget lost either way.

The grid test now sweeps short clips across 6 rates x 7 clip lengths x 4 frame
counts, asserting at most one frame is trimmed and the window still ends on the
cutpoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…bilise setNumFrames

Two leftovers from the review.

The continuity anchor is pinned last, so the arrows that would displace it fired
a patch and got the list straight back: the anchor's own up arrow, and the down
arrow of the card above it. Both looked enabled and did nothing (worse, the
patch remounted the list, since the keys embed the index). The card now takes
`canMoveUp` / `canMoveDown` instead of `isFirst` / `isLast`, and the list
excludes the moves the pin reverts.

`setNumFrames` listed `values.references` as a dependency, which is a fresh
array on every render, so the callback identity churned and the Frames slider
re-rendered on every patch anywhere in the panel -- against this file's stated
"every prop identity is stable by construction" contract. It now reads the list
through the ref added for the functional-update fix, which is safe precisely
because the re-derive is idempotent in `numFrames`.

The ref moves above both consumers: declared after them, react-compiler flagged
the effect's assignment as modifying a value already passed to a hook.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
… flag on load

Two identities that did not survive a boundary.

The latest-references ref crossed PROJECTS. The video widget is reconciled,
never remounted, across a project switch (the <Activity> key is the panel
instance, not the project), while `patch` stays bound to the project its render
belonged to. So the bare ref tracked whichever project was ACTIVE: drop a video
into project A's references, switch to B before the gallery resolve lands, and
the captured updater ran against B's list with A's patch -- replacing A's
reference list with B's entries plus the new one, invisibly until the user
switched back. The snapshot array this branch removed was at least
per-project-correct. The ref now carries `{ projectId, references }` and a
write whose project has moved on is dropped: losing one pending add because
the user switched projects mid-resolve beats overwriting a list they can see.

The anchor flag did not survive RECALL. `fromSourceVideo` is panel state and
never reaches metadata, so a recalled reference-extend panel arrives with its
anchor unflagged beside the source video -- and every invariant this branch
built keys on the flag. The pin skipped it (the model continued from whatever
reference followed), the frame-count re-budget skipped it (Frames 345 -> 90
left a 141-frame window and the backend cut 2.125s off the seam, the exact
failure this branch exists to prevent), and the seam-anchored start index fell
back to the absolute form. The cap-gate commit even taught its predicate to
tolerate the unflagged shape without noticing the other three invariants do
not.

The flag is derivable, not just storable: the video reference naming the
Initial Video's clip IS the anchor, the same identity the setter adopts by.
`sanitizeVideoReferences` now re-derives it -- before the pin, which is before
the cap trim, so a freshly flagged anchor is also immediately last and
protected from overflow. An already-flagged entry stays authoritative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…d say why

`setSourceVideo` detected the refusal -- `applyReferenceExtendSourceVideo`
returning its input untouched -- toasted about it, and then patched the clip in
anyway. That left the panel in the worst available state: an Initial Video set
with NO continuity anchor, generating an extension with nothing sampling the
clip, and the cap gate then closing over it so the trim sliders froze. The cap
gate cannot pre-empt this: it can only ask about the clip currently set, and
the refused drop is by definition a different clip. The drop is now refused
whole -- no partial application -- and the toast says what actually happened
("Initial video not set") instead of describing a reference that was never
created. Clearing is unaffected: removing the linked entry cannot overflow
anything.

And when the gate does close with a clip already set (reachable by deleting the
anchor card and filling the slots), the only symptom was frozen trim sliders:
`disabledReason` rendered exclusively in the empty-state slot, which a set clip
replaces with its preview. The trim block now shows the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
… newest

The render-time cap gates (`canAddVideo` / `canAddImage`) are evaluated before
the add handlers' await, and the Initial Video field is an independent writer
that can fill the slots during it. The over-cap write then reached
normalization, whose front-drop overflow deleted the user's OLDEST reference --
where the pre-branch keep-first rule had at least deleted the surplus add.
So a race turned into silent loss of the user's earliest work.

Two layers:

- The add updaters re-check the cap against the LIVE list at apply time. A
  declined add surfaces through the field's existing error line ("All 3 video
  reference slots filled while this one was loading, so it was not added")
  rather than vanishing. The updater form makes this exact: it runs against
  the same list the write would land on.
- Sanitization's overflow now drops the NEWEST non-anchor videos instead of
  the front. The surplus in any over-cap record is whatever arrived last; the
  anchor stays exempt wherever it sits, which was the one thing the front-drop
  got right. This also restores the pre-branch behaviour for anchor-less
  lists, which the front-drop had changed for no reason the rationale covered.

The race shape is pinned in a test: [B, C, anchor, D-raced-in] normalizes to
[B, C, anchor] -- the racing add is the casualty, not B.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…ndings

setSourceVideo was the one reference writer still resolving against a captured
`values.references` -- and it is called from an await (the clip field's adopt
and upload paths), so a reference added while an Initial Video drop resolved
was clobbered by the stale list. It now reads through the project-scoped ref
like its siblings, with the same drop-the-write guard for a project that moved
on, and its refusal identity check compares against the list it actually used.

tailSourceFrames gets its closed form: for an integer budget,
`floor(t*24/fps + 0.5) >= budget` is exactly `t >= (budget - 0.5)*fps/24`, so
the smallest count is a ceil away, with two at-most-a-step loops absorbing
last-ulp float disagreements. The previous walk down from `ceil(budget*fps/24)`
ran ~fps/24 iterations -- its own comment claimed the inverse bound -- which an
absurd-but-probeable container frame rate turned into 124 ms per Frames
keystroke. Verified value-identical across 3,952 (budget x rate) pairs
including NTSC rationals; the up-loop never fired.

The browser seed-toggle test now sets IS_REACT_ACT_ENVIRONMENT like every
other browser test, so its act() calls actually gate on effect flushing
instead of warning six times and passing on userEvent's own awaits.

Also DECIDED, recorded in toReferenceStartIndex's docstring: TAIL_INDEX_SLOP
stays at 3. An estimate error past the slop can still hard-fail the relative
start, but only on a clip barely longer than the window (it needs
error > startFrame), and 3 is this file's declared bound on estimate error.
A wider guard would move the cliff, not remove it, and pay with silent length
drift on every clip inside the wider margin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
Four fixes, two of them sharing a root.

The derived flag adopts the LAST same-name entry, not the first. The pin
invariant records the anchor last, and metadata can hold a user's own
reference to the source clip ahead of it -- a first-match flagged the user's
entry: their trim was re-budgeted, the list reordered against the recording,
and the true tail window sat unprotected at the seam, the exact bug the
derivation exists to fix, displaced onto the wrong reference.

The flag is canonicalized to at most one entry (keeping the last) before the
pin and the trim. Two flagged entries -- a corrupt or hand-merged record --
were a poisoned state: the pin swaps the pair on alternate normalization
passes, and the overflow trim exempts every flagged entry, so an over-cap
list of them could never come back under the cap. With one flag, both
invariants hold unconditionally. The drop set is also keyed by index rather
than object identity, so an aliased entry can no longer take its twin down
with it.

The fps guard gains an upper bound (1000). Past ~2^53 source frames the
tailSourceFrames adjustment loops cannot even step -- `tail + 1 === tail` in
floats -- and spin forever: a hand-edited record with fps 1e17 froze the tab
on the first Frames keystroke, verified with a real 60s hang. The closed-form
commit fixed the ~1e6-range loop cost and left this range hanging; absurd
rates now take the same fallback as broken ones.

The reference ref carries a LIVENESS bit, set in its sync effect and cleared
in that effect's cleanup. A hidden <Activity> destroys effects while promise
continuations keep running, so the ref froze with the projectId still
matching -- and a same-project guard replayed the frozen list over whatever
the store had done meanwhile, resurrecting references a gallery deletion
sweep had removed. A write now needs the ref both live and same-project.
setReferences also skips the patch when the updater returns its input
identity: a declined add no longer fires the spread's first/last-frame
clearing or dirties the project with a no-op write. Comments corrected to
say effect-fresh rather than live -- two resolves in one microtask drain
still share a window, which is inherent to reading through React state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
…gh-rate clips

The float plateau the bound guards (`tail + 1 === tail`) starts around fps
1.5e15; the bound was 1000 -- twelve orders tighter than the failure. Every
rate in between terminated in the closed form and produced a correct window
before the guard existed, and the clamp broke it: a probe-reported 1200 fps
clip took the 24 fallback, the frontend sized a 141-SOURCE-frame window, and
the backend -- which resamples at the rate it probes itself, with no fps field
in the graph to override it -- collapsed that window to 3 frames, under text
conditioning's 13-frame minimum. A working input became a hard invoke-time
ValueError with nothing pointing at the clamp.

1e6 kills the hang with nine orders of margin and no reachable regression.
The boundary test now actually pins the behaviour on both sides: 1200 fps on
a 50001-frame clip derives its real 7025-source-frame window (previously
asserted through a fixture too short to distinguish the clamp from its
fallback), and 1e17 takes the fallback and terminates.

Found by the fourth adversarial pass, scoped to the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDrDMYKpzD8h5PRy5kLjzH
Base automatically changed from feat/minimax-h3-model-positions to main September 1, 2026 18:25
@lstein
lstein enabled auto-merge September 1, 2026 18:26
@lstein
lstein merged commit c33e98c into main Sep 1, 2026
19 checks passed
@lstein
lstein deleted the feat/minimax-h3-ref2v-extend branch September 1, 2026 19:08
lstein added a commit that referenced this pull request Sep 1, 2026
…cord baselines post-merge

Merging main brought in the Ref2VA reference-extend UI (#191), whose
VideoReferenceListField carried two new upload call sites hard-coded to board
'none' — the same bug this PR fixes in the other video-panel fields. They now
use the video port's getUploadBoardId(). The reference list's video upload
input also accepts audio/* — the server wraps audio files into waveform
videos, which is how audio-only reference clips enter the pipeline.

Performance baselines re-recorded on the merged tree (both sides of the merge
had re-recorded them independently) and formatted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4B5exWsbC2z2Uu2tA167A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant