LLM Prompt Tests / Bioluminescent Abyssal Temple / Failure analysis

Ornith 9B —
why the run failed

The Ornith 1.5 9B 8-bit build never gets past its own "Descending…" loading screen. No frame is ever rendered. This is the full post-mortem: what crashed, where, and why the smaller model's architecture choices both helped and doomed it.

Model — Ornith 1.5 9B 8-bit Tool — Grok Build CLI Date — 2026-08-19 Status — Run failed View run page →
TL;DR

An ambitious, modern Three.js build — that was never run.

The script crashes with an uncaught exception during initial scene construction, before the first frame. Because the loading overlay is only faded after the first successful render, the page sits on "Descending…" forever, with no error surfaced in the UI. Patching the first crash immediately reveals a second fatal crash in the animation loop. Two non-fatal logic bugs are hiding behind those.

Root cause · Fatal bug 01

deckWidth referenced out of scope

Console output when loading the file (headless Chrome, console piped to stderr):

Uncaught ReferenceError: deckWidth is not defined  (line 559)

deckWidth is declared as a const inside makeBridge():

function makeBridge() {
  // ...
  const deckWidth = 4.6;          // line 507 — function-scoped to makeBridge()
  // ...
  const lantern = makeLantern(z); // line 550 — calls a *separate* function
}

…but referenced inside makeLantern(), a different top-level function where it does not exist:

function makeLantern(z) {
  const g = new THREE.Group();
  const lx = rand(-deckWidth * 0.35, deckWidth * 0.35);  // line 559 — ✕ ReferenceError

makeBridge() is invoked at module top level (line 581), so execution dies right there. Everything after — kelp, coral, jellyfish, fish, bubbles, rays, the moon aperture, the composer, the animation loop — never runs.

Why the loader never clears: the "Descending…" overlay is only hidden after the first rendered frame:

// Fade out loading screen once the first frame renders.  (lines 973–978)
renderer.render(scene, camera);
requestAnimationFrame(() => {
  loadingEl.classList.add('hidden');
  animate();
});

Line 974 is never reached, so the overlay stays up indefinitely. A nice pattern — but it also means the failure is completely silent in the UI.

Waiting behind bug 01 · Fatal bug 02

userData.path shape mismatch

With bug 01 patched in a scratch copy, the next error fires every frame:

Uncaught TypeError: Cannot read properties of undefined (reading 'speed')  (line 916)

makeFish() stores the swim-path object directly on userData:

const path = { cx, cz, radius, height, speed };
g.userData = path;                 // line 740 — userData IS the path

…but the animation loop reads it through a .path property that was never set:

fishes.forEach((f) => {
  const p = f.group.userData.path; // line 915 — undefined
  const a = t * p.speed + p.cx;    // line 916 — ✕ TypeError
});

Either g.userData.path = path (write side) or const p = f.group.userData (read side) would fix it. As shipped, the two sides of the data contract disagree.

Latent defects

Non-fatal bugs hiding behind the crashes

01

Inverted gap guard line 549

if (z > gapEnd && z < gapStart) continue; — gapStart (≈17.1) is always less than gapEnd (≈22.9), so this condition can never be true. The guard is dead code and lanterns spawn across the broken section of the bridge. Intended: z > gapStart && z < gapEnd.

02

Pause button doesn't pause line 960

if (!paused) controls.update(); — the flag only gates the OrbitControls damping update. Every animation (jellyfish, fish, kelp, bubbles, dust, rays, lanterns, lights) keeps running while the button claims "Resume".

03

Per-frame RNG in the light-ray update line 949

r.mesh.position.set(…, rand(-80, 30)) re-rolls a random z-position every frame, so the god-ray cones teleport instead of swaying smoothly. The seeded RNG was clearly meant for build-time placement.

04

Dead instances in coral / sea-fan loops lines 631, 652

continue skips setMatrixAt for positions near the temple approach, leaving those instance slots zero-initialized — they collapse to a degenerate point at the origin. Invisible, but wasteful and sloppy.

05

Style smells

The lantern bulb material is created twice (lines 568–569), and makeLantern() adds itself to the scene (line 576) only to be re-parented into the bridge group by its caller (line 551) — works by accident because Three.js silently re-parents.

Head to head

9B 8-bit vs. the working 35B 4-bit run

Ornith 1.5 9B 8-bit (this run)Ornith 1.5 35B 4-bit MoE (working)
Module systemES modules + importmap, Three 0.160.1 + addonsClassic script, global THREE r128
Code structureMany small top-level functions, const/let block scopeOne giant IIFE, var, single shared scope
Post-processingEffectComposer + UnrealBloomPassNone (additive glow materials instead)
CameraOrbitControls (damped auto-orbit)Hand-rolled spherical orbit + drag/zoom
RandomnessSeeded RNG (mulberry32) — deterministic sceneMath.random() — different every load
ExtrasFPS counter, gradient shader dome, caustic shimmer planeFocus button, touch pinch zoom
Pause behaviorBroken (gates camera only)Correct — wraps all updates
ResultCrashes before first frameRuns

Where 9B's approach was genuinely better

  • Modern stack done right (on paper) — ES modules, import maps, the official addons for OrbitControls and UnrealBloom. This is how a Three.js project should be written in 2026; the 35B file uses a 2021-era global build.
  • Determinism — the seeded RNG means every visitor sees the same scene. The right instinct for a prompt test artifact.
  • Performance hygiene — instanced coral and sea fans, shared box geometry, glow sprite textures instead of extra lights.

Where 35B's approach saved it

  • One shared scope — every helper lives in the same IIFE; lanternXs, lanternLights, doorLight, chasmLight are declared once at the top and visible everywhere they're used. The 9B file's split into many small functions is architecturally nicer — but the model lost track of which variables live in which scope, which is exactly what killed it (deckWidth).
  • Simpler moving parts — no post-processing chain, no module graph, fewer cross-function data contracts; fewer places for a shape mismatch like userData.path to hide.
Verdict

9B wrote the better blueprint; 35B shipped the working product. The prompt test measures shipped products.

The 9B output contains two independent fatal runtime errors that any single execution — even just opening the file once — would have exposed instantly. Neither bug is subtle; both throw on the very first code path. The 9B model produced a convincing looking program (good comments, sensible structure, considerate loading UX) without ever checking that it works. For a prompt test whose prompt says "actually generate the complete working HTML file," that's a failed task regardless of architectural taste.