DockRailLazyModule.spec.mjs:121 fails with instances expected 1, received 2 — a lazily-resolved pane constructed twice. Seen on two unrelated PRs (#18176 at ~08:56Z, one operationChangeClass entry; #18200 at 12:12Z, two theme custom-property values), neither of which can double-construct anything.
Correction 2026-09-03 (author). The first version of this ticket proposed a single-flight guard keyed by item and made it AC-3. The operator named that correctly as working around the problem: a mutex makes the duplicate harmless without asking why two loads are dispatched, and baking it into an AC would have shipped the suppression. The fork it offered ("engine race versus fixture over-counting") is also dead — see below. Problem, Fix and ACs are rewritten around the actual cause.
The Problem
Neo.layout.Card#loadModule is not idempotent, and it already owns the marker that would make it so.
asyncloadModule(item, index) {
…
item.isLoading = true; // prevent the item from getting queued multiple times inside form.Containermodule = awaitmodule(); // ← the window
…
items[index] = item = Neo.create(item); // ← the construction
It setsitem.isLoading and never reads it. There is no early return anywhere in the method: set at :208, await at :210, delete at :217, return at :231.
The flag's only reader in the whole engine is one unrelated consumer:
So the contract is inverted. isLoading was introduced so that somebody else would not queue the item again — its own comment says so — while the function that performs the load does not protect itself. form.Container compensates externally; every other caller has no such check.
Two callers therefore both proceed:
the first sets isLoading, then awaits the dynamic import;
during that await the second arrives, sees nothing blocking, sets isLoading again, awaits, and both reach Neo.create(item).
The second items[index] = … then overwrites the first instance, so the defect is not only a duplicate construction — one live component is orphaned in the process.
Why it looks like a flake: the window is exactly the duration of a dynamic import, which widens under machine load. A passing re-run does not mean nothing happened; it means the second call arrived outside the window.
The fixture is exonerated by reading, so the original fork is closed.lazyPaneInstances reads a static counter incremented inside construct(), one increment per construction:
There is no path on which it over-counts. instances: 2 is two constructions.
The Architectural Reality
src/layout/Card.mjs:197loadModule(item, index) — the single owner of a parked item's load, with the unguarded marker.
src/form/Container.mjs:80 — the flag's only reader, and the reason it exists at all.
src/container/Base.mjs (#18084 / #18086) — the second caller. Its insert path starts the load itself when the inserted index is already active, guarded by me.layout.activeIndex === index and justified in its comment as "an index that is already active fired that hook long before this insert".
That justification is an assumption about ordering, not a fact — which is the second, smaller finding here. It is correct whenever activation genuinely preceded the insert, and wrong when the un-hide both inserts the item and (re-)activates its index, which is exactly the scenario the failing arm drives: the lazy item becomes the only visible item of its tabs node. But this ordering assumption is only reachable as a defect becauseloadModule does not defend itself; with the owner idempotent, a caller that asks twice gets one construction and the heuristic stops mattering.
This is engine-general, not dock-specific. Any container with a card layout and a parked lazy module has it. The dock is where it surfaced because #18086 added a second legitimate caller.
The Fix
Make the owner idempotent, at the owner.
loadModule returns the in-flight promise when a load for that item is already running, so both callers settle on the same instance rather than racing to create two. The parked config is a plain object that later passes through Neo.create, so the promise belongs in a private WeakMap on the layout keyed by that config rather than on the config itself — isLoading stays the public marker form.Container reads.
Explicitly not the fix:
A caller-side mutex or an item-keyed single-flight registry. That is the workaround: it leaves two owners each believing they must start the load, and adds a coordination mechanism to make the duplicate survivable rather than impossible.
Removing the insert path's load. Both callers are legitimate. An insert at an already-active index genuinely must load, because no activation follows it.
Tightening activeIndex === index. It would narrow the window without closing it, and the assumption it encodes is unfalsifiable from inside insert().
form.Container's external check becomes redundant but stays: it avoids queueing work at all, which is cheaper than joining a promise, and removing it is a separate simplification.
Acceptance Criteria
AC-1 — two concurrent loadModule calls for the same parked item produce one construction and both resolve to the same instance. Unit-level, driving layout.Card#loadModule directly with a deferred import so the window is explicit rather than load-dependent.
AC-2 — red-first: the same arm reds on current source with two constructions, and the second call's instance being discarded is asserted, not just the count.
AC-3 — a single loadModule call is unchanged: one construction, isLoading deleted, the item created and referenced at its index exactly as today.
AC-4 — a failed import leaves no in-flight entry, so the next activation retries. The current catch-side contract (delete item.isLoading, config stays parked) must survive, and a rejected load must not poison the WeakMap.
AC-5 — form.Container's reader still sees isLoading while a load is in flight; its external check is untouched by this change.
AC-6 — DockRailLazyModule.spec.mjs:121 passes, and the arm is no longer sensitive to machine load. Stated as a consequence, not as the acceptance instrument: AC-1 is what proves the fix.
AC-7 — unit/container/InsertLazyModule.spec.mjs and the card-layout suite stay green.
Out of Scope
#18179, the DockPreviewGeometry sibling in the same job. Same intermittency class, different subject, no shared mechanism established.
The insert path's activeIndex === index heuristic. Named above as a second finding; it stops being reachable once the owner is idempotent, and narrowing it is a separate change with its own evidence.
The machine load. It widens the window; it does not create it. A contract that holds only on an idle machine is not held.
Avoided Traps
Calling it a flake and re-running. Two occurrences on unrelated diffs, and a counter that increments once per construct. The re-run passing is the window closing, not the defect being absent.
Loosening the arm to tolerate 2. The single-construction contract is the point of the spec — #18084 exists because a lazy pane was mishandled on insert — and tolerating the duplicate would retire the guard while keeping its name.
Guarding at the callers. The original version of this ticket did exactly that; it is recorded above rather than deleted, because the reasoning that produced it is the reasoning to avoid.
Assuming the fixture. Ruled out by reading the counter's only increment site, not by argument.
Decision Record impact
none — an idempotency repair inside one method.
Related
#18084 / #18086 — added the second legitimate caller; the ordering assumption lives there.
#18179 — the sibling intermittent red in the same CI job.
#18176, #18200 — the two unrelated PRs it has reddened.
Handoff Retrieval Hints
query_raw_memories: "loadModule sets isLoading and never reads it, lazy pane constructs twice"
Anchors: layout/Card.mjs:197 (loadModule), layout/Card.mjs:208 (the unguarded marker), form/Container.mjs:80 (its only reader), container/Base.mjs insert-parks path
Context
DockRailLazyModule.spec.mjs:121fails withinstancesexpected 1, received 2 — a lazily-resolved pane constructed twice. Seen on two unrelated PRs (#18176 at ~08:56Z, oneoperationChangeClassentry; #18200 at 12:12Z, two theme custom-property values), neither of which can double-construct anything.The Problem
Neo.layout.Card#loadModuleis not idempotent, and it already owns the marker that would make it so.async loadModule(item, index) { … item.isLoading = true; // prevent the item from getting queued multiple times inside form.Container module = await module(); // ← the window … items[index] = item = Neo.create(item); // ← the constructionIt sets
item.isLoadingand never reads it. There is no early return anywhere in the method: set at:208,awaitat:210,deleteat:217, return at:231.The flag's only reader in the whole engine is one unrelated consumer:
// src/form/Container.mjs:80 if (Neo.typeOf(item.module) === 'Function' && !item.isLoading) {So the contract is inverted.
isLoadingwas introduced so that somebody else would not queue the item again — its own comment says so — while the function that performs the load does not protect itself.form.Containercompensates externally; every other caller has no such check.Two callers therefore both proceed:
isLoading, then awaits the dynamic import;isLoadingagain, awaits, and both reachNeo.create(item).The second
items[index] = …then overwrites the first instance, so the defect is not only a duplicate construction — one live component is orphaned in the process.Why it looks like a flake: the window is exactly the duration of a dynamic import, which widens under machine load. A passing re-run does not mean nothing happened; it means the second call arrived outside the window.
The fixture is exonerated by reading, so the original fork is closed.
lazyPaneInstancesreads a static counter incremented insideconstruct(), one increment per construction:construct(config) { super.construct(config); this.constructor.instances++ }There is no path on which it over-counts.
instances: 2is two constructions.The Architectural Reality
src/layout/Card.mjs:197loadModule(item, index)— the single owner of a parked item's load, with the unguarded marker.src/form/Container.mjs:80— the flag's only reader, and the reason it exists at all.src/container/Base.mjs(#18084 / #18086) — the second caller. Its insert path starts the load itself when the inserted index is already active, guarded byme.layout.activeIndex === indexand justified in its comment as "an index that is already active fired that hook long before this insert".That justification is an assumption about ordering, not a fact — which is the second, smaller finding here. It is correct whenever activation genuinely preceded the insert, and wrong when the un-hide both inserts the item and (re-)activates its index, which is exactly the scenario the failing arm drives: the lazy item becomes the only visible item of its tabs node. But this ordering assumption is only reachable as a defect because
loadModuledoes not defend itself; with the owner idempotent, a caller that asks twice gets one construction and the heuristic stops mattering.This is engine-general, not dock-specific. Any container with a card layout and a parked lazy module has it. The dock is where it surfaced because #18086 added a second legitimate caller.
The Fix
Make the owner idempotent, at the owner.
loadModulereturns the in-flight promise when a load for that item is already running, so both callers settle on the same instance rather than racing to create two. The parked config is a plain object that later passes throughNeo.create, so the promise belongs in a privateWeakMapon the layout keyed by that config rather than on the config itself —isLoadingstays the public markerform.Containerreads.Explicitly not the fix:
activeIndex === index. It would narrow the window without closing it, and the assumption it encodes is unfalsifiable from insideinsert().form.Container's external check becomes redundant but stays: it avoids queueing work at all, which is cheaper than joining a promise, and removing it is a separate simplification.Acceptance Criteria
loadModulecalls for the same parked item produce one construction and both resolve to the same instance. Unit-level, drivinglayout.Card#loadModuledirectly with a deferred import so the window is explicit rather than load-dependent.loadModulecall is unchanged: one construction,isLoadingdeleted, the item created and referenced at its index exactly as today.delete item.isLoading, config stays parked) must survive, and a rejected load must not poison the WeakMap.form.Container's reader still seesisLoadingwhile a load is in flight; its external check is untouched by this change.DockRailLazyModule.spec.mjs:121passes, and the arm is no longer sensitive to machine load. Stated as a consequence, not as the acceptance instrument: AC-1 is what proves the fix.unit/container/InsertLazyModule.spec.mjsand the card-layout suite stay green.Out of Scope
#18179, theDockPreviewGeometrysibling in the same job. Same intermittency class, different subject, no shared mechanism established.activeIndex === indexheuristic. Named above as a second finding; it stops being reachable once the owner is idempotent, and narrowing it is a separate change with its own evidence.form.Container's now-redundant external check.Avoided Traps
construct. The re-run passing is the window closing, not the defect being absent.Decision Record impact
none— an idempotency repair inside one method.Related
#18084/#18086— added the second legitimate caller; the ordering assumption lives there.#18179— the sibling intermittent red in the same CI job.#18176,#18200— the two unrelated PRs it has reddened.Handoff Retrieval Hints
query_raw_memories: "loadModule sets isLoading and never reads it, lazy pane constructs twice"layout/Card.mjs:197(loadModule),layout/Card.mjs:208(the unguarded marker),form/Container.mjs:80(its only reader),container/Base.mjsinsert-parks pathOrigin Session ID: e8ea32a2-b9a8-43ea-84ba-b83081ec4b3d