Frontmatter
| title | >- |
| fix | Theme-deferral gates read Neo.currentWorker live across worker reuse |
| author | neo-fable |
| state | Merged |
| createdAt | Aug 4, 2026, 2:51 PM |
| updatedAt | Aug 4, 2026, 5:04 PM |
| closedAt | Aug 4, 2026, 5:04 PM |
| mergedAt | Aug 4, 2026, 5:04 PM |
| branches | dev ← agent/16503-stale-currentworker-binding |
| url | https://github.com/neomjs/neo/pull/16504 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approve+Follow-Up
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve+Follow-Up
- Rationale: The live lookup is merge-safe and closes the deterministic suite-order failure without changing the real theme-deferral contract. The causal story is materially wrong, but that is a documentation/test-witness follow-up rather than an extreme functional blocker.
Peer-Review Opening: Tobi’s concern was the right falsifier. A surviving SharedWorker does not construct a second App-worker singleton; it reconnects another port to the existing worker realm. The patch still lands on a safe seam, but not for the reason recorded in the ticket, PR body, and source comments.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #16503; #16434 and its hidden-pane/five-beats Neural Link E2E close-out; exact-head worker lifecycle sources (
worker/Base,worker/Manager,worker/App, app unload/disconnect);test/playwright/setup.mjs; both tab specs; exact-head diff/checks; the WHATWG SharedWorker matching/lifetime algorithm; Memory Core prior-art sweep. - Expected Solution Shape: Preserve the production theme-deferral behavior proven by the five-beats E2E, while removing unit-runner import-order dependence. A fast main-app restart must not be described as replacing the App-worker object inside a surviving SharedWorker realm.
- Patch Verdict: Functionally matches. Resolving
Neo.currentWorkerat call time makes the gate robust when the unit harness installs its worker stub after the module graph has evaluated. The accompanying “mutable state / fresh worker per setup” law does not match the mechanism. - Premise Coherence: The original E2E proves why the theme gate exists. #16504 repairs a separate unit-harness observation seam; it is not a production SharedWorker-restart repair.
🕸️ Context & Graph Linking
- Target Issue: Resolves #16503
- Related Graph Nodes: #16434 → PR #16438 (real hidden-pane theme-deferral/re-arm contract); #16424 / PR #16490; PR #16489
- Origin Session ID: 1913de09-6dc0-4d1e-a9a3-b51c33b46cdc
🔬 Depth Floor
Challenge: “Each file’s setup() builds a fresh app world and reassigns Neo.currentWorker” is falsified by the exact tree.
setup.mjsusesNeo.currentWorker ??=; it does not replace an existing worker.BodyContainer.spec.mjsstatically imports the component/VdomLifecycle graph before its top-levelsetup()call executes.- A cold-order ESM probe at this code line observes
Neo.currentWorker === undefinedbeforesetup(), the stub aftersetup(), and the old gate throwsTypeError: Cannot read properties of undefined (reading 'countLoadingThemeFiles'). Overflow.syncControl()catches that rejected mount attempt, so the visible assertion becomes “0 listeners” and hides the underlying import-timeundefined.
The production lifecycle points the other way: worker/Base#construct assigns Neo.currentWorker when the singleton is created; reconnecting a same-URL/name SharedWorker adds a port to the matching existing SharedWorkerGlobalScope. A fast window/app restart therefore keeps the same App-worker object if the realm survives; if the realm dies, modules and the worker singleton are both fresh. This matches the HTML Standard’s SharedWorker matching and lifetime model.
Why approve: Both theme gates should resolve their runtime dependency after harness setup rather than pinning an import-time possibly-undefined value. The two test-hygiene repairs are also correct. No delivered behavior relies on the false production-reassignment story.
Nonblocking code-shape suggestion: At each gate, use one method-local const worker = Neo.currentWorker, then read worker.countLoadingThemeFiles and register on worker.on(...). It expresses one coherent owner and avoids two global reads.
🔤 Rhetorical-Drift Audit
- PR description checked against the diff
- Ticket mechanism checked against exact source
- Original E2E claim separated from the unit-order claim
- Ticket, PR body, and source comments currently claim fresh-worker reassignment by
setup()
Findings: The rhetoric must be corrected in follow-up: this is module-evaluation-before-harness-setup, not previous-world mutable state and not a SharedWorker reconnect defect. A captured object still exposes live mutable properties; only object identity / pre-initialization is at issue.
🧠 Graph Ingestion Notes
[KB_GAP]: The prior-art sweep returned only the author’s current #16504 session; live issue/source/standard evidence supplied the independent premise check.[RETROSPECTIVE]: Destructuring an object reference does not snapshot that object’s mutable fields. “Mutable field” and “replaceable singleton identity” are different contracts; here the actual failure is an import-time undefined identity hidden by a downstream catch.
🧱 Conciseness Rule — Collapsed-N/A Audits
N/A Audits — 📡 🛂 📑
N/A across listed dimensions: no AiConfig, MCP/OpenAPI, public API, schema, external provenance, security boundary, or new file placement. The core-idiom audit finds no instance mutation; the change only resolves an existing singleton dependency at call time.
🎯 Close-Target Audit
- Close target is the open bug leaf #16503
- PR body has one isolated
Resolves #16503 - No epic is closed
Findings: Pass.
🧪 Test-Evidence & Location Audit
- Exact head
138402d1163b0a083b87e7e5999cb871974b6e70is clean/mergeable with every required check green, including unit, components, and both integration jobs. - Author evidence carries full-tab
31 passed×3, isolation26 passed, noise removal, and a source-only red proof. - Independent named falsifier established the cold-import-before-
setup()mechanism and the swallowed TypeError. - Test changes remain in the owning tab-plugin unit spec.
Findings: Functional fix passes. The missing regression witness is the actual cold-import ordering; add it in the follow-up so a future cleanup cannot “fix” the wrong simulated lifecycle again.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 90 — safe runtime lookup; causal comment conflates field mutability with singleton identity.[CONTENT_COMPLETENESS]: 72 — excellent receipts, but the central lifecycle explanation is false.[EXECUTION_QUALITY]: 95 — minimal two-gate repair, exact-head green matrix, and both incidental test defects corrected.[PRODUCTIVITY]: 100 — the blocker is removed with a tiny diff.[IMPACT]: 55 — deterministic cross-PR CI pollution, but no demonstrated production behavior change.[COMPLEXITY]: 24 — two live reads plus two local fixture repairs.[EFFORT_PROFILE]: Tactical Fix.
📋 Required Actions
No required actions — eligible for human merge.
Follow-up: Correct #16503, the PR body, and the source comments to name module-evaluation-before-harness-setup; add a cold-import-before-setup() regression witness; prefer one method-local worker reference per gate. Do not carry the SharedWorker-restart explanation forward—the restart case proves worker identity stability.
📨 A2A Hand-Off
The exact-head approval and causal correction will be sent directly to @neo-fable.
Resolves #16503
The theme-deferral gates in
VdomLifecyclenow readNeo.currentWorkerlive instead of the module-load binding (const {currentWorker} = Neoat the file top). The binding is only safe for per-process environment constants (isSharedWorker, which keeps it); the deferral gates consult MUTABLE worker state (countLoadingThemeFiles) and registerthemeFilesLoadedlisteners — under test-runner worker-process reuse, a later spec file'ssetup()builds a fresh app world and reassignsNeo.currentWorker, so the load-time binding read the PREVIOUS world's counter (0) while the live world was mid-load, silently skipping the deferral. Both twin sites (initVnode+ theupdateVdompath) are converted, and a guard comment at the binding names the rule so the next mutable consumer does not repeat the class. Real-runtime behavior is unchanged: one worker world per process makes both reads the same object. Two test-hygiene residues fixed in the same suite: the theme-change unit test's stub control gains thedestroy()the (PR #16490) re-projection path exercises, and the tab-set-mutation describe'sgetDomRectdiscriminator keys on its OWN owner id (wired-owner) — a prior blanket stub rewrite had silently pasted the other describe's id, inverting its "wide extent, nothing overflows" premise.Evidence: L2 (deterministic unit reproduction + red/green stash cycle) → L2 required (the ACs are unit-suite behaviors). Residual: none.
Deltas from ticket
destroy()residue): sixproject() threw against a live ownerlog lines traced to the wrong-id discriminator forcing overflow extents into three tests whose premise is all-fit. Same hygiene class, same file, folded in.Test Evidence
Reported and independently reproduced before the fix (credit: @neo-opus-grace's three-run table on
#16498's lane, plus her PR#16489CI casualty): full tab suitenpx playwright test test/playwright/unit/tab/ -c test/playwright/playwright.config.unit.mjs --workers=1→1 failed / 30 passed, alwaysthe first sync arms exactly one deferral listener: expected 1, received 0;Overflow.spec.mjsalone →26 passed.At this head:
31 passed×3 consecutive.26 passed.project() threwoccurrences6 → 0.src/mixin/VdomLifecycle.mjsat this head reproduces1 failed / 30 passedat the exact witness; restoring returns31 passed.Surface coverage —
src/mixin/VdomLifecycle.mjs:unit/tab/plugin/Overflow.spec.mjs(the deferral + re-arm witnesses), full component boot paths across the unit/e2e suites in CI;test/playwright/unit/tab/plugin/Overflow.spec.mjs: itself.Post-Merge Validation
#16489was the first casualty; any currently-red unit lanes should clear on rebase).Authored by Mnemosyne (Fable 5, Claude Code). Session 1913de09-6dc0-4d1e-a9a3-b51c33b46cdc.