From f803b1a329b17fa0be471e2338cd214293d2e2d7 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:01:52 +0300 Subject: [PATCH] fix: refuse an overlapping generate() instead of corrupting the in-flight run's state Round 3 of adversarial review found that GenerateBridge now holds exactly ONE run's shared state (_genRunPrefix/_genRunSceneIds/_genAdoptedRun, window.GenerateBridge's own module-level ranFor/ranForList), but only the main Generate/Selected/Render buttons are gated against starting a second one while the first is still queuing/running (actionbar.js's busy(st)). H3 Combo Sweep's "Run sweep" button has no such gate and calls _generateRun directly -- if clicked while another generation (including one adopted on reload) is already in flight, it silently overwrote that run's scene attribution and status message, got refused deeper inside (session.js's own starting/phase guard), and left the REAL run's eventual result recorded onto the sweep's scene instead of its own when it finished -- persistent data corruption, not just a UI glitch -- and could leave _genAdoptedRun stuck false, reopening round 1's "adopted run's clock never stops" bug. Fixed at the root: _generateRun itself now checks state.gen.state before touching any of that shared state, and refuses (loudly, via console.warn) rather than clobbering it. Fixes every current and future caller in one place, not just block_sweep.js -- consistent with every other caller already being a sequential, awaited loop that this guard doesn't affect. Live-verified: two overlapping Store.generate() calls now produce exactly one real attempt and one logged refusal, with the first call's status never overwritten. 691/692 JS tests and 548/548 Python tests pass (the one JS failure is the pre-existing, unrelated projects.test.js import-race flake). Co-Authored-By: Claude Sonnet 5 --- app/legacy/store.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/legacy/store.js b/app/legacy/store.js index 2d521973..750f2735 100644 --- a/app/legacy/store.js +++ b/app/legacy/store.js @@ -3701,6 +3701,20 @@ return false; } _wireGenerateBridge(); + // GenerateBridge holds exactly ONE run, and _genRunPrefix/_genRunSceneIds/ + // _genAdoptedRun belong to whichever call is currently driving it. A + // second call reaching here while a first is still queuing/running (the + // main Generate/Selected buttons are gated against this by `busy(st)` in + // actionbar.js, but H3 Combo Sweep's own "Run sweep" button is not) would + // silently overwrite those with ITS scene/prefix, then get refused deeper + // inside (session.js's own starting/phase guard) -- leaving the real run's + // eventual result recorded onto the WRONG scene when it finishes, and its + // clock stop possibly skipped. Refused here, loudly, before any of that + // shared state is touched, rather than corrupting it silently. + if (state.gen && (state.gen.state === "queuing" || state.gen.state === "running")) { + console.warn(`[FunPack] "${prefix || "a generation"}" was refused: another generation is already in flight.`); + return false; + } // A run started through the normal click path owns its own clock // lifecycle (the _clockedX wrapper's `finally`, spanning the whole // batch) — any stale flag left over from a PRIOR adopted run must not