studio-worker 0.4.7

Pull-based image-generation worker for the minis.gg studio.
Documentation
# No-fallback policy

> Companion to the studio-side
> [`apps/studio/docs/model-registry.md`]https://github.com/webbertakken/minigames/blob/main/apps/studio/docs/model-registry.md.

This worker used to silently route any job to its `SyntheticEngine`
when no real engine claimed the requested model.  That fallback was
destructive on a live queue: real-model jobs returned placeholder
bytes that looked indistinguishable from real renders.  As of the
"per-game image config" PR the fallback is gone.

## What changed

1. **`MultiEngine::dispatch_with_source` routes strictly by
   `ModelSource.engine`.**  The TS studio resolves the model row from
   D1 (`studioModels.engine`) and attaches it to every offer.  The
   worker picks the engine whose `name()` matches the chosen
   `engine` slug and bails out otherwise:

   ```
   no `sdcpp` engine compiled into this worker (model z-image-turbo-q4_k_m.gguf requires it)
   ```

2. **`task` + `model_source` are required on every offer.**  Top-level
   `prompt` + `ext` on the claim are gone; the worker reads kind-aware
   payloads through the `Task` enum and refuses (with a
   `protocol_violation`) any claim that lacks either field.

3. **`JobClaim::resolved_task` is deleted.**  There is no legacy
   `prompt`-to-`Task::Image` synthesis path on the worker side.

## What stays

The `SyntheticEngine` is still compiled into the worker by default.
It serves models whose `ModelSource.engine == Synthetic` and only
those.  Use cases:

- **CI**.  The GitHub Actions matrix has no GPU; the full WS contract
  + multi-modal round-trip suite use synthetic to exercise the wire
  contract on free-tier runners.
- **Bootstrap**.  A fresh worker install responds immediately, before
  the 5 GB of real-model weights finishes downloading.
- **Pipeline-only verification**.  Operators with a small GPU can
  confirm the offer / claim / complete loop works against
  `synthetic-image` before troubleshooting their real-model VRAM
  budget.

## Diagnosing a refused offer

When the orchestrator sends a `Fail` because no engine accepted, the
WS log line looks like:

```
worker fail jobId=… reason="no `sdcpp` engine compiled into this worker"
```

Action items in order of likelihood:

1. The worker binary was built without the `sd-cpp` feature flag.
   Rebuild with `cargo build --release --features image-sdcpp` (or
   the engine's actual feature gate).
2. The studio's `studioModels.<id>.engine` value doesn't match any
   engine compiled into this worker (`sd-cpp`, `llama-cpp`,
   `synthetic`).  Open the Models page and confirm.
3. The studio's offer carries `engine: 'synthetic'` but the worker
   has explicitly removed the synthetic engine via a custom build.
   Re-add it or stop offering synthetic models.

## Why this matters

Silent fallback to synthetic produced two failure modes that were
particularly hard to diagnose from operator logs:

- **Asset poisoning** — placeholder WebP bytes landed in R2 under a
  real asset name + version, marked `done`, and were served to game
  clients indistinguishably from real renders.
- **Tail-eating** — a worker that had advertised support for a real
  model but couldn't actually serve it would keep claiming jobs,
  returning synthetic bytes, and starving the queue of any real
  worker that could take them.

Both are now compile-time impossible: the dispatch trait takes a
non-`Option<&ModelSource>`, and `MultiEngine` never falls back.