services/build-worker runs the trusted prefab compiler in a separate Node process and coordinates revision-checked results. Both npm run author -- build-prefabs … and expand-prefabs … now use this path. A build produces immutable content; it does not mutate the running game or activate a release.
#Using the coordinator
import { BuildCoordinator } from './services/build-worker/coordinator';
const builds = new BuildCoordinator();
const ticket = builds.submit({target: 'development', revision: '0', source});
const outcome = await ticket.done;
if (outcome.state === 'published') {
const release = builds.current('development')!;
// release.build.world is ordinary world-2 content for Simulation.create().
}
await builds.close();A target is a logical build slot, not a path or room identifier. revision is a canonical decimal uint64 build-request sequence, independent of an authoring document's revision. Increase it for a new attempt, even if retrying identical source after a failure. The highest admitted sequence stays fenced after failure or cancellation. An exact duplicate of that highest request returns the original ticket and outcome. Reusing its sequence with different input, or requesting an older sequence, reports a conflict. Admission failures do not advance the sequence or cancel accepted work.
Submitting a newer request supersedes an older unfinished request for the same target. The old child is cancelled; even an executor that ignores cancellation and replies later cannot publish over its successor. current(target) retains the last successful build while a replacement is queued, running, rejected or cancelled. A failed replacement also retains the previous accepted result. There is no await between final generation verification and accepted-pointer replacement.
Statuses are immutable snapshots: queued, running, published, failed, superseded or cancelled. status(id) reads current or retained history. ticket.done resolves to a terminal status, including failures; invalid admission throws before a ticket is created. Cancellation/supersession may report a terminal state before a child has finished exiting. Concurrency and input-byte accounting remain reserved until that process closes. close() stops admission, cancels queued/running work and waits for process cleanup. stats() reports owned slots and serialized-byte accounting.
The BuildCoordinator above remains local and in memory. Use the durable queue below when requests must survive a worker restart or several processes share build targets. Normal callers use the process executor; injecting another executor is a trusted integration/testing extension. Neither API is an authentication boundary or a room-publication command.
#Worker protocol and bounds
prefab-job.1 carries bounded JSON source, compiler settings and an input SHA-256 incorporating both compiler versions. The worker independently verifies that hash, runs the compiler, and returns either the complete artifact or a structured diagnostic. The parent verifies the response's input binding, compiler/settings fields, semantic build hash and a second whole-artifact hash that includes source mappings. These checks detect wrong requests and transport/artifact corruption; they are not cryptographic proof that an untrusted compiler executed correctly.
Defaults:
| Resource | Limit |
|---|---|
| Concurrent child processes | 2 |
| Pending jobs, including terminating children | 32 |
| Pending request bytes | 32 MiB |
| Retained successful artifact bytes | 32 MiB |
| Target revision domains | 32 |
| Completed status history | 128 |
| One complete worker request | 16 MiB |
| One worker response | 16 MiB + 4 KiB |
| Child stderr | 8 KiB |
| Wall-clock deadline, including startup | 30 seconds |
| Child V8 old-space limit | 256 MiB |
The coordinator permits bounded configuration changes. Source/output/expansion limits in the prefab compiler still apply. The process executor bounds streams before parsing; it terminates a child on timeout, abort or excess output and waits for the close event before resolving. A CPU-blocked child is terminated from the parent. Request-provided paths never choose a worker executable or import code. NODE_OPTIONS is not inherited by the compiler process, so it cannot silently override configured launch limits.
The V8 limit is not a total RSS/container-memory cap: native memory, buffers and engine overhead are additional. Serialized-byte counters are not heap measurements. Use OS/container resource controls for a deployment-wide ceiling. The worker executes trusted engine code with the ordinary OS permissions of its parent; it is not the future creator-script sandbox. The durable prefab queue below now provides saved requests, expiring attempt ownership, restart recovery and verified artifact reuse. The queue dispatches prefab, model and texture jobs to their existing bounded worker protocols; no source-provided path can select an executable.
#Packaged CLI and Docker
npm run build:author
node author-dist/author.js build-prefabs games/engine-zoo/prefab-kit.json /tmp/new-kit-build.jsonThe build emits author.js, worker.js, shared code and an ESM package marker in author-dist/. It runs outside the repository with Node 22.23.4 and no development dependencies. For tests, node tools/cli/build-author.mjs <empty-output-directory> writes a separate bundle; explicit nonempty directories and symlinks are rejected. The normal production build includes this bundle, and the Docker runtime copies it into /app/author-dist. The room server does not start compiler jobs automatically and no new network endpoint is exposed.
The source CLI selects the TypeScript worker with the development loader. The packaged CLI selects its sibling JavaScript worker. Other production consumers can construct processBuildExecutor({worker: new URL('file:///…/worker.js')}) and supply it to the coordinator. Source-based default construction is intended for the development environment.
Output files still use exclusive creation and are validated before opening. Pretty formatting is used when it fits within the 16 MiB file limit; near the limit the CLI writes compact JSON so its outputs remain readable by the bounded file reader. Filesystem crash durability and atomic publication of release directories are separate unfinished work.
#Verification
tests/build-worker.test.ts uses real compiler children and fault processes. It covers input ownership, duplicate tickets, exact sequences above 2^53 and at uint64 exhaustion, stale-result fencing, last-good retention, bounded admission/publication/history, queued cancellation, actual CPU-hang termination, process disappearance after deadline/abort, heap-limit launch configuration, excess stdout/stderr, malformed replies and checksums. It builds the standalone distribution into a temporary directory and runs it outside the repository. The prefab CLI tests now execute the same process path while retaining their standalone-world/native-restore checks.
Docker acceptance additionally runs the packaged author CLI inside the non-root production image, builds the saved environment kit through its isolated worker, and checks all nine compiled objects before continuing the existing multiplayer, persistence and restart checks. This does not replace the engine roadmap's remaining release, recovery or asset-import gates.
npm run bench:build measures sequential full-Darkwind process builds (three warmups, ten samples), verifies all 1,480 authored object/identity mappings, and restores the resulting 1,481-entity native runtime exactly. It reports the source commit, machine, process configuration and ownership counters; it does not establish concurrent capacity or a durable recovery objective.
Clean-source acceptance records 152 passing headless tests plus successful core and Docker CI on 2a24a38; browser CI remained in progress. The full-world benchmark on benchmark-only follow-up e78f488 measured 490.714 ms median / 531.077 ms p95 across ten builds, preserving canonical JSON object/identity data and exactly restoring the 1,481-entity runtime. The file boundary normalizes signed zero; the measurement compares that canonical representation. These timings include process startup and do not establish parallel build capacity.
#Durable build queue — October 27
DurableBuildQueue stores prefab build requests, revision fences, job status and
the last successful artifact in PostgreSQL. It uses separate checksum-checked
build tables; it does not apply control/room migrations. Prefer a dedicated
build database. This is an operator API, with no public admission endpoint.
Set DW_BUILD_DATABASE_URL to that database, then:
npm run build:queue -- submit workshop 0 games/engine-zoo/prefab-kit.json
npm run build:queue -- work
npm run build:queue -- status
npm run build:queue -- export workshop /tmp/workshop-build.json
npm run build:queue -- release workshop /tmp/workshop-game.jsonrelease packages the saved successful world through the public game SDK. Start
an isolated player with DW_GAME_FILE=/tmp/workshop-game.json npm run dev; do not
also set DW_PLAYTEST. Export/release refuse existing output files. They create
local artifacts, never activate a production room. The packaged equivalent is
node author-dist/build-queue.js … and needs no development dependencies.
Use work --watch for a supervised worker. SIGTERM/SIGINT stops its compiler and
returns unfinished work to the queue. Abruptly killed attempts become reclaimable
after the default 45-second lease. After three expired attempts, the job fails
with a visible diagnostic; submit a higher revision to try again. Compiler
processes retain their existing 30-second deadline and 256 MiB V8 limit.
An exact repeat of the latest target/revision/input returns its saved ticket, even after restart. A different input at that revision conflicts. A newer revision supersedes unfinished work; cancelled, expired and replaced workers cannot publish. Compilation and publication failure retain the previous successful build. The accepted pointer and completed status commit together. Artifacts with an identical compiler/settings/source hash can be reused after validation.
The queue admits 32 targets and at most 32 pending latest requests / 32 MiB of serialized pending input. Retained worker replies are bounded to 32 MiB, in addition to their decoded published JSON representation. Each target's latest ticket and the newest 128 other terminal tickets are retained. Terminal requests release source bytes. This is not a global child-RSS or distributed-concurrency limit: size the number of worker services and container limits explicitly. A superseded worker can finish computing until its own deadline; its result is fenced.
#Docker / Coolify build worker
compose.builds.yaml starts an isolated PostgreSQL database and a private worker.
Set DW_BUILD_POSTGRES_PASSWORD to a URL-safe random password (for example, a hex
secret); it is embedded in the private database connection URL. Docker creates
darkwind-build-db automatically. Preserve that named volume across redeployments.
Use a separate Coolify Compose resource with this file and branch
codex/engine-foundations; no domain or public port is needed. Operator submissions
can use docker compose -f compose.builds.yaml exec build-worker node /app/author-dist/build-queue.js …. Source paths refer to files inside the container;
mount operator-selected source files read-only when they are outside the image.
No deployment has been performed by this change.
#Focused acceptance and remaining work
Five PostgreSQL checks passed: restart/duplicate/conflicting revision and cache reuse, late superseded results and cancellation, actual worker-process kill and reclaim, standalone packaged submission/export/game-release, and graceful stop / expired-result fencing. The checks use the real compiler subprocess. The package runs outside the checkout, and the existing standalone author CLI compatibility check also passed. Type/boundary/wire checks and Compose configuration validation passed. The exported release passed the physical-GPU two-traveler browser check in 11 seconds; screenshot shows the compiled fixture, not a character/environment art milestone.
Model and texture queue integration is delivered below. Existing assistant provider requests remain separately permissioned and are not automatically resubmitted. Remaining qualification: actual container execution, database outage during admission/publication, long multi-worker runs, storage capacity/pruning behavior and deployment resource limits. Those checks are deferred; the focused passes do not close the full asset pipeline or production gate.
#Durable model and texture imports
The same saved queue now accepts GLB models and PNG/JPEG textures. Original bytes, import settings, usage and versioned input hashes are committed with the request. Workers verify the original importer/compiler reply before publishing. Restart, revision conflicts, cancellation, last-good retention, attempt limits and verified cache reuse use the same queue contract as prefab builds.
npm run build:queue -- import-model lantern 0 games/engine-zoo/assets/lantern-plinth.glb
npm run build:queue -- import-texture wood 0 /path/to/wood.png color
npm run build:queue -- work
npm run build:queue -- export lantern /tmp/lantern.model.json
npm run build:queue -- export wood /tmp/wood.texture.jsonModel submission accepts an optional settings JSON file after the GLB path, for
example {"scene":null,"unitScale":0.01}. Texture usage is color, color-opaque,
data or normal; changing usage/settings changes the request hash. status
identifies each job as prefab, glb or texture. release remains a world-build
command and refuses standalone assets. Exports use exclusive, synced-file
publication so a failed replacement never truncates an existing output.
In the editor, choose Models → Import model and select the exported model JSON. The same preview, placement, Save, Undo and reload workflow applies. Original GLB files remain supported. Prepared JSON is validated in the import worker and limited to 6 MiB, matching the existing compiled-model budget; GLB source stays limited to 8 MiB. The queue's larger exported artifacts may therefore need reduced geometry/textures before editor use. A corrupted model is rejected before staging. Standalone texture artifacts are for the existing texture/model pipeline; the model browser does not accept a texture JSON as a model. Imported GLB texture maps remain part of their model artifact.
DurableBuildQueue.submitModel, submitTexture and artifact expose the same
operator workflow in code. current retains its prefab-only API and refuses an
asset target explicitly. No new network endpoint or provider authorization was
introduced. Keep original source files and licensing/provenance records outside
the queue: terminal jobs release the queued source bytes, while accepted artifacts
retain their source/settings identities.
#Upgrade and focused acceptance
Stop all old build workers before upgrading. Queue schema version 2 adds the job kind while preserving existing requests and accepted results. Initialization checks both immutable migration checksums; it does not change the room schema. Old worker versions must not run against the upgraded queue. Back up the build database before upgrading; code rollback alone does not reverse its schema. The existing Docker definition needs a rebuilt image and retains its named volume.
Ten PostgreSQL checks passed, covering the five prefab queue cases plus model settings/source ownership/cache and restart, texture usage/mips/bad replacements, an actual interrupted model worker, schema-1 preservation, and packaged asset export outside the source tree. Exported models/textures pass the normal artifact readers. Type/boundary/wire checks passed. The physical-GPU editor imports the actual packaged-queue model, rejects a changed checksum, previews it, saves/reloads it and undoes it (4.1 seconds); original GLB import passed its focused compatibility check (5.8 seconds). Reviewed saved model.
Deferred qualification remains: actual container execution, large/animated assets, JPEG through the durable deployment, mixed job saturation, storage failures and multi-worker rollout. The focused fixture is functional asset-pipeline evidence, not final artwork approval. Existing assistant generation/provider requests keep their current explicit retry and permission rules.