16-reviewer audit of ae815f489 (103 findings) remediated in six groups.
1. Lazy THREE init (runtimeobject.ts): scratch Box3/Matrix4/Euler allocated on
first use, so a 2D-only export (never loads three.js) boots.
2. Culling correctness: computeAABB uses |size|/2 half-extents and places the box
at the true rotation pivot C' = P + R*(C-P); custom-object children union
conservative boxes; the frustum is rebuilt in the boxes' space (right-multiply
_threeGroup.matrixWorld) with a numeric cache incl. camera-Z; hide/cull route
through setCullingVisible/applyRenderVisibility (no detached-carrier writes);
custom-object children and nested tile maps no longer cull with a wrong window.
3. Studio pipeline wedge (Utils.js + Studio/*): lock release over all calls,
working marks only editor calls, spawn failures reach the model, plan merge on
objects (JSON.parse bug), one persistent LoopGuard with real call_id +
re-arming + recordSuccess, RequestWriteGate self-retry + supersede-rejects,
UseStudioRuntime fire-and-forget gate write + recomputed plan + sent check,
FinalizeSubAgents terminal-'error' + executed-but-unsent + turn-cap + enriched
report, send-returns-boolean, BYOK cache write-through, suspension cascade,
turn-cap dispatch, stand-alone cannot spawn.
4. Instancing: useInstancing now serializes (C++); the pool grows without null.add
and collapses deleted slots; the instance matrix is normalized
(carrier * correction) and rewritten on rotation/hide; animated/skinned/morph/
multi-mesh models rejected; per-slot shadows; layer moves re-acquire.
5. BYOK role enforcement: studioRoleId persisted and re-applied every turn (tools +
prompt); dispatch filters tools outside the role; readOnly roles skip edit
approval.
6. Warnings + Info: mutating-tool classification, hasOwnProperty role guards,
cache-message merge, spawn arg caps, per-batch GDD note, fan-out cap, plan
dependsOn-omission + caps + duplicate-id + fail-closed + trimmed ids, tilemap
rotated/mirror bail + window margin + null-map guard, pool dispose wired to
unloadScene, schema description required, plus doc/comment corrections.
Verified: engine builds clean; boot + computeAABB (13/13) + Model3DInstancePool
(9/9) harnesses pass; Jest green for src/AiGeneration + src/AI (175), CustomAIClient
(27, incl. the critical second-turn role regression), PlanStore (25),
FinalizeSubAgents (21), LoopGuard (18), RequestWriteGate (11), Roles (9).
Answers the two questions the fork asked, then acts on both.
Assessment (AI_GAME_STUDIO_ASSESSMENT.md): a literal Anno-1800-scale 3D
city builder is not achievable on this engine - no instancing, no 3D
culling, Android is a Cordova WebView around the same HTML5 runtime, and a
15 MB per-resource cloud-build ceiling. A 2D isometric city builder of
Anno-1602/1701 scope is reachable today. The hosted backend already is a
multi-agent company; the BYOK path can reach none of it (no plan tool, no
sub-agent tool, no role prompts, no local spawner, no loop guard).
Studio (phases 2-5, 9-11), all under newIDE/app/src/AiGeneration/Studio/:
* Roles.js - manager/designer/developer/tester: prompts, exact tool
subsets, read-only invariant, turn caps.
* PlanStore.js - create_or_update_plan is real under BYOK and returns the
shape getLatestActivePlan/OrchestratorPlan already read; the dispatcher
merges a new plan over the existing one so a re-plan cannot strip a field
another writer set.
* SpawnSubAgents.js + customCreateSubAgentAiRequest - spawn_agent (the BYOK
tool schema goes 29 -> 39 tools) and a real child AiRequest carrying the
role's prompt and tool subset.
* FinalizeSubAgents.js + UseStudioRuntime.js - a finished child's report is
written onto its parent's function_call_output and the plan task flips.
* RequestWriteGate.js - one gate owns writes to an AiRequest, with named
block reasons and latest-wins per-request serialization.
* LoopGuard.js - a local circuit breaker (repeated identical calls, error
storms, turn count) with a steer -> constrain ladder. Ported from
chaitanyagiri/munder-difflin (MIT) src/main/breaker.ts, incl. its #377
whole-arguments sha256 key; no MD source is vendored.
* One in-flight model turn per request id, and the two in-place mutators
made whole-replace so a concurrent turn cannot discard them.
Engine (phases 6-8):
* 3D culling: gdjs.Object3DCulling shared AABB maths, a per-layer frustum
cache, and a 3D branch in both _updateObjectsPreRender implementations.
Capability-checked, because CustomRuntimeObject3D does not extend
RuntimeObject3D.
* Instancing: opt-in Model3D "Use instancing" property + Model3DInstancePool,
one InstancedMesh per (resource, material, layer) for single non-skinned
meshes; every other model silently keeps the clone path.
* TileMap culling generalised to TileMap (not just SimpleTileMap) and to
tilemaps nested in custom objects, with bounds clamped to the map.
Two bugs found by running the loop end-to-end rather than by tests: the
instance pool was page-global (handing out slots on a discarded scene's
mesh), and buildSubAgentReport read only `output_text` while the local path
emits `text`, so every local sub-agent reported "(no report)".