fix(ui): seed Wan params fields in the v3->v4 persist migration (release-upgrade params wipe) - #9408
fix(ui): seed Wan params fields in the v3->v4 persist migration (release-upgrade params wipe)#9408lstein wants to merge 7 commits into
Conversation
FindingsMedium: incomplete fix - upgrades from v6.12.0 and older still wipe the slice. Evidence chain:
The PR body acknowledges this as out of scope, but the title/summary claim to fix the "release-upgrade params wipe", and the same one-line-per-field seed in the existing v2 -> v3 block would close it. Anyone upgrading from v6.12.0 or earlier still loses prompts, model selection, and dimensions. To expose this issue, add a test that migrates a v2 blob built from an explicit v6.12.0-release key list (not Medium: the new regression tests do not lock in what their comments claim, and cannot catch the next occurrence. The fixture is described as a "field-accurate released-build v3 blob", but it is To expose this issue, add a test that derives the fixture key set from an explicit per-release constant (or a checked-in JSON snapshot of a real v6.13.x blob) and, better, a schema-completeness test that asserts every top-level key of Low: A single missing nullable key discards the user's entire persisted params slice with only a |
The Wan video PR (invoke-ai#9163) added five keys to zParamsState (wanTransformerLowNoise, wanComponentSource, wanVaeModel, wanT5EncoderModel, wanGuidanceScaleLowNoise) while the persisted params schema was still at _version 3, without a version bump or migration seed. The keys are .nullable() with no .default(), which zod treats as required, and migrate() ends with zParamsState.parse() whose failure makes the store silently replace the slice with its initial state. Released v6.13.x builds write v3 blobs without these keys, so any user upgrading from a release to a build containing Wan loses their entire params slice (prompts, prompt history, model selection, dimensions, generation settings) on first launch. Dev machines don't reproduce it because v3 blobs written after the Wan merge already carry the keys, and the migration test fixtures spread getInitialParamsState() which carries them too. Seed the five keys conditionally (?? null) in the v3->v4 step so released-build blobs migrate cleanly while dev-build blobs keep any values they already hold. Add a field-accurate released-build v3 fixture that fails without the seed, plus a test that existing Wan values survive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review feedback on invoke-ai#9408. Finding 1 (incomplete fix): the v3->v4 Wan seeds fixed upgrades from v6.13.x, but releases v6.10.0 - v6.12.0 persist _version 2 blobs, and 15 keys added to zParamsState after v3 was cut are required (no .default(), .optional() or .catch()) and seeded nowhere: fluxDype{Preset,Scale, Exponent}, zImageShift, zImageSeedVariance{Enabled,Strength, RandomizePercent}, anima{VaeModel,Qwen3EncoderModel,Scheduler}, klein{VaeModel,Qwen3EncoderModel} and qwenImage{ComponentSource, Quantization,Shift}. Seed them conditionally in the v2->v3 step, so released v2 blobs migrate cleanly and dev-build v2 blobs keep the values they already hold. Verified by running the real migrate() over a blob built from the v6.10.0 release key set: it threw on exactly those 15 paths before this change. Finding 2 (tests can't catch the next occurrence): the fixtures spread getInitialParamsState(), so they carry every current key and are inert against the general defect. Replace them with the top-level zParamsState key sets as actually shipped, read out of the release tags and checked in, one per persisted version still in the wild (v6.10.0 for v2 and v6.13.7 for v3 - each the narrowest key set among the releases writing that version, so a subset of every real blob). Add a schema-completeness test that runs the version steps over each release blob and asserts no key of the current schema is left unhandled, naming the offending keys and the step to fix. It fails on any future required-no-default key added without a seed. Finding 3 (fail-open-and-destroy): a single missing key made zParamsState.parse() throw, and the caller in store.ts falls back to the initial state, wiping prompts, model selection and dimensions with only a log.warn. Add backfillMissingParamsKeys(): after the version steps, fill any key that is absent and that the schema cannot fill itself, and warn with the key names. Narrow by design - a key that is present but invalid still throws, and anything with a .default()/.catch()/.optional() is left to zod. So a forgotten seed now costs one field at its default instead of the user's whole params slice. The completeness test above deliberately bypasses the net so it still fails CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up from an adversarial review of the previous commit.
The v2 fixture was not the narrowest v2 release. The earlier survey
globbed tags as v6.1*, which silently excluded v6.7.0 - v6.9.0 — four
stable releases that also persist _version 2, with only 46 keys against
v6.10.0's 52. Six further keys are required-with-no-default and seeded
nowhere: fluxScheduler, zImageScheduler, colorCompensation,
zImageVaeModel, zImageQwen3EncoderModel and zImageQwen3SourceModel.
Verified by running the version steps over a v6.7.0-shaped blob: parse()
throws on exactly those six. Seed them in the v2 -> v3 step and replace
the fixture with the true narrowest set (v6.7.0, confirmed a strict
subset of v6.10.0/v6.11.x/v6.12.0). Add a v6.6.0 (_version 1) fixture
too; it is the v6.7.0 set minus positivePromptHistory, which the v1 step
already seeds.
Also close three edges the safety net did not cover:
- The v0 step dereferenced state.dimensions.rect unguarded, so a blob
lacking dimensions threw a TypeError straight out of migrate() — the
one remaining path that could still wipe the slice. Guard it and let
the backfill repair dimensions instead.
- The v0 branch tested key presence (!('_version' in state)) while the
backfill tests value (!== undefined). A blob with an explicit
undefined _version matched no branch, reached the parse and took the
slice down. Detect v0 by value so the two agree.
- Exclude _version from the backfill loop, so a future change cannot
turn it into a version-detection bypass that stamps a blob current
without running a single step.
Each fix is mutation-checked: reverting any one of them fails at least
one test, and the six seeds fail the schema-completeness test.
Not covered: v6.2.0a1 - v6.5.1 persist a blob with no _version at all
and predate the current dimensions shape, so a faithful fixture can't be
built by filtering getInitialParamsState(). Noted in the test file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8d4247e to
4d0ccb0
Compare
|
Thanks — all three findings confirmed and fixed, and finding 1 turned out to be wider than either of us had it. Rebased onto Finding 1 — incomplete fix, v2 blobs still wipeConfirmed. Your v6.12.0 probe found 7 keys; the real number is 21, because v6.12.0 is the widest v2 blob, not the narrowest. Four stable releases persist
Running the version steps over a v6.7.0-shaped blob throws on 21 paths — your 7, plus All 21 are now seeded in the v6.6.0 ( Finding 2 — fixtures were inertAgreed, and thanks for the precise diagnosis. Fixtures no longer derive from
Picking the narrowest is the whole point: my first pass at this used v6.10.0 as the v2 fixture and missed six keys precisely because it isn't the oldest v2 release. On top of the per-release migrate tests, the schema-completeness test you asked for: applyParamsVersionMigrations(blob);
const unseeded = backfillMissingParamsKeys(blob);
expect(unseeded).toEqual([]);For each released blob version it asserts that no top-level key of the current schema is left unhandled — a key is acceptable only if the schema can fill it itself ( Finding 3 — fail-open-and-destroyFixed rather than deferred, scoped to this slice. After the version steps,
The obvious risk is that the net makes the completeness test vacuous, since it repairs exactly what that test looks for. So that test deliberately runs Three edges the net did not cover, also closed:
I did not change the generic Not coveredv6.2.0a1 – v6.5.1 persist a blob with no VerificationEvery fix is mutation-checked — reverting any one of them fails at least one test, and dropping the 21 seeds fails the completeness test. |
|
This fixes the reported problem, and the core of it is well-evidenced. I verified the migration Verified correct
Worth a follow-up PR
None of 2-4 blocks this PR. Item 1 is the one I would not leave open for long, since it only gets |
Addresses the four follow-ups from Pfannkuchensack's second review. The v4 tier was unguarded: the PiD fields landed a day after the _version 3 -> 4 bump, so dev builds from that window persist v4 blobs without them, and a v4 blob matches no branch in the migration chain. Give the four fields zod defaults, matching the ernieImage* precedent set by the other two post-bump additions, and pin a RELEASE_PARAMS_KEYS entry to the bump commit's key set so the invariant covers the tier no version step can reach. Add v0 fixtures. The claimed v0 range was wrong: it spans v6.0.0a1 - v6.6.0rc2, and the oldest builds have no `dimensions` key at all, which no step seeded — the invariant only held there because the safety net caught it. Seed `dimensions` in the v0 step and cover both v0 shapes. Widen the safety net from omissions to any key whose persisted value fails its own field schema, so a `dimensions` the v0 guard left incomplete, or a `model` whose base has since left zBaseModelType, costs that one field instead of the whole slice. This makes true what the guard's comment already claimed. Fix the comments that still described the presence check this branch replaced with a value check. Also close three holes in the tests themselves, all found by mutating the production code and watching nothing fail: the _version guard test never reached the guard (the version steps normalise _version first), the PiD test could not distinguish the new defaults from the safety net backfilling the same values, and the fixtures carried initial values throughout — including `model: null`, so nothing noticed a model being silently cleared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — all four are in this PR rather than a follow-up. Two of them turned out to be wider than reported, and item 2 in particular was hiding a real invariant violation. 1 — The v4 tier is unguardedConfirmed, including the dating: the bump is Fixed by giving the four PiD fields zod defaults rather than bumping to v5. That follows the precedent already set in this tier: I took the structural suggestion too: 2 — No v0 fixture in the invariantAdded, and the exercise justified itself: the v0 tier was actually violating the invariant, and the safety net was the only thing hiding it. First, the range in my own comment was wrong. Reading
So v0 runs to v6.6.0rc2, not v6.5.1; v1 includes v6.7.0rc1; v2 runs through v6.13.0.rc1. (My v1/v2/v3/v4 fixtures are unaffected — v6.7.0 really is the intersection of all 21 v2 builds, and v6.13.7 of all 7 v3 builds.) The consequence:
Second, and this applies to your fixture as much as mine: a v0 fixture asserting only that 3 — Stale comments describing the presence checkFixed both, and I folded your second point into the comment rather than dropping the value check: // Value check rather than `!('_version' in state)`. The two are equivalent on real input, since
// the only production caller feeds this `JSON.parse` output, which cannot produce an explicit
// `undefined` — but a value check is what the rest of this file uses, and with a presence check a
// blob carrying `_version: undefined` would match no branch at all, reach the parse and take the
// whole slice down.4 — The
|
Summary
Upgrading from any released v6.13.x build to current
mainsilently wipes the user's entire persisted params slice (prompts, prompt history, model selection, dimensions, all generation settings) on first launch. This PR fixes it by seeding the five Wan component fields in the v3→v4 params persist migration.Root cause
The Wan video PR (#9163) added five keys to
zParamsState—wanTransformerLowNoise,wanComponentSource,wanVaeModel,wanT5EncoderModel,wanGuidanceScaleLowNoise— while the persisted schema was still at_version: 3, without a version bump or a migration seed. All five are.nullable()with no.default(), which zod 4 treats as required: a persisted blob missing the key failsparse().migrate()ends withzParamsState.parse(state), and when that throws,unserialize(store.ts) catches it and silently replaces the slice withgetInitialState().Released v6.13.x builds write v3 blobs without these keys (no release contains the Wan merge), so a release → main upgrade hits: migrate runs → all seeds applied except wan →
parse()throws on exactly the five wan paths → whole slice wiped, with only alog.warn. Verified with a field-accurate pre-Wan v3 blob: parse fails on precisely those five keys and nothing else.Two effects masked this:
getInitialParamsState(), which carries the keys, so the suite couldn't catch it.The fix
Seed the five keys conditionally (
?? null) in the existing v3→v4 step. Conditional, because v3 blobs written by dev builds after the Wan merge can already hold real values (e.g. a selected Wan VAE) that must not be clobbered.Tests:
Related
colorCompensation,fluxDype*,anima*,zImageSeedVariance*— affecting upgrades from pre-v6.13 releases), and the throw-and-wipe failure mode itself may be worth revisiting (repair-and-log, or a.default(null)convention for nullable slots). Kept out of scope here to keep this release-blocking fix minimal; can file a follow-up issue.QA Instructions
mainwithout this PR: params are reset on first launch.Or:
pnpm vitest run src/features/controlLayers/store/paramsSlice.test.ts.Checklist
🤖 Generated with Claude Code