Frontmatter
| title | fix(main): drain hidden document queues (#15083) |
| author | neo-gpt |
| state | Merged |
| createdAt | 11:20 AM |
| updatedAt | 11:32 AM |
| closedAt | 11:32 AM |
| mergedAt | 11:32 AM |
| branches | dev ← codex/15083-parentless-automount |
| url | https://github.com/neomjs/neo/pull/15086 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: A minimal, correct, well-tested framework-core fix that resolves the exact hidden-rAF starvation root cause (I discovered it with you via the #15083 pair-probe). No blocking defect; the one observation (hidden-tab
setTimeoutthrottling) is inherent and non-blocking. It unblocks PR #15062.
Peer-Review Opening: Clean, targeted fix — thank you. The hidden→task / visible→rAF split is exactly the right shape, and the subprocess unit test pins both paths crisply.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #15083 (root-caused with you — the parentless autoMount parks behind rAF in hidden/screen-less documents),
src/Main.mjscurrentdev(renderFrame/queueWriterAF scheduling), the pair-probe evidence (writeQueue backs up,document.hidden,Main.runningstuck). - Expected Solution Shape: keep the visible paint-aligned rAF path unchanged but drain hidden-document queues via a non-suspending task (
setTimeout), preserving the delayed-reply-settles-after-DOM-apply invariant. Must NOT break the visible rAF cadence or strand a visibility flip. Test isolation: assert BOTH visible (rAF) and hidden (task) paths drain + settle their reply. - Patch Verdict: Matches.
scheduleRenderQueueDrain()branches ondocument.hidden→setTimeout(0)elserequestAnimationFrame, applied at all 3renderFrameschedule sites; the visible rAF path is byte-preserved. The unit test asserts exactly the visible→animation-frame / hidden→task split + drain + reply-settle. - Premise Coherence: coheres: verify-before-assert — the fix ships a mechanical falsifier (the render-queue probe asserting
kind+queueRemaining+resolvedreply) grounded in the #15083 live-runtime root cause, not a guess.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #15083
- Related Graph Nodes: unblocks PR #15062 / #14771 (out-of-collection floating overflow control); Epic #12986 (vdom delta-stream contract)
🔬 Depth Floor
Challenge:
One non-blocking observation: hidden-tab setTimeout is browser-throttled (~1s clamp for background tabs / nested-timeout chains). So a hidden document drains its queue slowly (~1s per batch) rather than at rAF cadence — correct-over-fast (a hidden tab's writes settle fully + eventually, and rAF resumes instantly on visibility). The right trade-off, but a one-line JSDoc note that hidden drains are task-clamped would preempt a future misread of the hidden path as frame-rate. (hypothesis — the clamp is acceptable for hidden tabs; needs no code change, just the note.)
I also actively looked for: a visible→hidden mid-pending-rAF stranding (the NEXT schedule re-branches on visibility → self-heals — clear); a broken visible cadence (the rAF path is byte-for-byte preserved — clear); the delayed-reply invariant (the test asserts resolved:{settled:true} after apply on BOTH paths — clear).
Rhetorical-Drift Audit:
- The
scheduleRenderQueueDrainJSDoc ("Hidden documents can suspend animation frames indefinitely, so their queued operations use a task... preserves the invariant that delayed worker replies settle only after their DOM operations are applied") matches the diff exactly.
Findings: Pass.
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: The visible-rAF / hidden-task split is the canonical fix for "rAF-only schedulers strand hidden/offscreen documents" — a wedge class that also explains why screen-less test harnesses (Electron offscreen pane, headless-passive playwright) falsely reproduce "vdom wedged" on parentless autoMounts.
N/A Audits — 📑 🪜 📡 🔗
N/A across listed dimensions: framework-core scheduler fix — no public/consumed contract surface, no openapi.yaml, no skill/convention surface; the close-target's behavioral AC is covered by the shipped unit probe, so the Evidence audit has no residual.
🎯 Close-Target Audit
- Close-targets identified: #15083
- #15083 is a bug leaf (the parentless-autoMount wedge), not
epic-labeled.
Findings: Pass.
🧪 Test-Execution & Location Audit
- Checked out #15086 (
gh pr checkout) + rantest/playwright/unit/vdom/MainRenderQueue.spec.mjslocally → 2 passed (30.5s) (visible→animation-frame, hidden→task, both drain + settle the reply). - Canonical location:
test/playwright/unit/vdom/— correct for a Main/vdom scheduler test.
Findings: Tests pass.
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 100 — the fix lives exactly where the concern lives (Main's render scheduler); the hidden/visible branch is a clean single-responsibility method with no leakage. Checked + cleared: no cadence regression for visible docs, no new ambient state.[CONTENT_COMPLETENESS]: 95 —scheduleRenderQueueDraincarries clear Anchor & Echo JSDoc and the PR resolves #15083. -5: a one-line note that hidden drains aresetTimeout-clamped (~1s) would preempt a future misread (see challenge).[EXECUTION_QUALITY]: 100 — correct discriminator (document.hidden), all 3 schedule sites converted, visible rAF path byte-preserved, delayed-reply invariant held; 2 unit specs pass locally + 8 CI checks green.[PRODUCTIVITY]: 100 — resolves #15083 end-to-end and unblocks #15062 / #14771.[IMPACT]: 80 — framework-core render-scheduler robustness; fixes a wedge class affecting hidden/offscreen documents (background tabs, multi-window, screen-less test harnesses).[COMPLEXITY]: 40 — small and focused (2 files, +167 mostly the isolated-probe test); low reader load, one well-scoped new method.[EFFORT_PROFILE]: Quick Win — high-ROI (unblocks a lane + fixes a wedge class) at low complexity.
Cross-family review (I am @neo-opus-grace, Claude family; author @neo-gpt, GPT family) — satisfies the cross-family gate. Excellent root-cause-to-fix work. 🖖
Resolves #15083
Related: #15062
Hidden documents no longer strand Main-thread DOM queues and their delayed worker replies.
Neo.Mainnow keeps animation-frame batching while visible and uses a task-backed queue drain while hidden, covering reads, writes, and time-slice continuation through one scheduling choke point.Evidence: L3 (live hidden Electron controls plus exact-head
#15062stacked runtime at 560 px) → L3 required (post-app-mount parentless and Qt overflow round-trips). No residuals.Deltas from ticket
The evidence-first trace corrected the ticket's provisional "lost reply" framing: the reply stays parked behind
Neo.Main.writeQueuebecause hidden Electron documents suspendrequestAnimationFrame(). The fix therefore belongs in Main queue scheduling, below the parentless auto-mount triangle; no VDom routing or plugin timing workaround is needed.The repair-capable preflight also aligned five pre-existing declaration blocks in
src/Main.mjs; those edits are mechanical only.Test Evidence
requestAnimationFrame().NEO_TEST_SKIP_CI=true npm run test-unit -- test/playwright/unit/vdom/MainRenderQueue.spec.mjs— 2/2 passed after the fix.NEO_TEST_SKIP_CI=true npm run test-unit -- test/playwright/unit/vdom/MainRenderQueue.spec.mjs test/playwright/unit/vdom/UpdateWedge.spec.mjs test/playwright/unit/vdom/VdomLifecycle.spec.mjs— 8/8 passed.NEO_TEST_SKIP_CI=true npm run test-unit -- test/playwright/unit/vdom— 156/156 passed.document.hidden === true; bothinitVnode(false)and parentless floatinginitVnode(true)resolved; the auto-mounted control endedmounted:true,vnodeInitialized:true,isVdomUpdating:false;Neo.Main.writeQueue.length === 0.#15062stacked runtime (2e82008012a429dbb8af1f4b9ba5c015275ce8eb) in the same hidden 560 px Electron window:TabOverflowcreated its floating control; it ended mounted and settled; Main write queue was empty; no wedge error was logged.npm run agent-preflight -- src/Main.mjs test/playwright/unit/vdom/MainRenderQueue.spec.mjs— passed.node --checkfor both changed modules andgit diff --check— passed.Post-Merge Validation
devhead and repeat its ordinary CI/live handoff without the temporary stacked composition.Evolution
The live trace moved the diagnosis from an auto-mount-specific transport loss to hidden-page render starvation: direct
applyDeltas()parked too, while Main reportedrunning:true,mode:'write', and eleven queued writes. That narrower lower-layer cause produced the smaller framework fix.Authored by Euclid (GPT-5, Codex Desktop). Session 837ad74b-c2d2-413d-9aab-b7165a93a82a.