Symptom
Under alchemy dev (2.0.0-beta.63), a worker binding a Durable Object from another worker via scriptName gets persistent 503s:
Durable Object "EvalHost" defined in worker "eval-host-dev" not found. Make sure the worker is running locally and exports the class.
Setup: the documented container pattern — an async consumer worker + a small Effect worker hosting Cloudflare.DurableObject + Cloudflare.Container (Containers.layer), bound cross-script:
env: {
EVAL_CONTAINER: Cloudflare.DurableObject<CfDurableObject>("EvalHost", { scriptName: `eval-host-${stage}` }),
}
Deployed stages work. Local dev worked on 2.0.0-beta.61 and is broken on 2.0.0-beta.63 (beta.62 untested here; it already contains the relevant rewrite, see below).
Live forensics (three parallel alchemy dev stacks: app, eval, opt)
- Registry (
~/.local/state/alchemy/registry/) contains entries only for the three subscriber workers (app-website-dev, eval-worker-dev, optrun-worker-dev). The two Effect host workers (eval-host-dev, opt-host-dev) have no entry at all — hence RegistryProxy.worker's resolution finds nothing and returns the 503.
- Process table: 8 workerd processes. Only three carry
--debug-port=127.0.0.1:0 — exactly the three registered workers (Runtime.start passes {"debug-port": "127.0.0.1:0"} unconditionally and its context.start(ports) performs the registry.write). The host workers exist only as their stable-port proxy instances; no real (debug-port) instance is running for them.
- Dev output: the host workers log
[host-worker] Changes detected, interrupting existing instance and the resource applies report updated — but a [host-worker] Started in …ms line never appears (the sibling API workers log [worker] Started in 3023ms normally). The hosts' runtime.start never completes.
The publish machinery itself is complete and correct in @distilled.cloud/cloudflare-runtime 0.13.5: RegistryProxyLive derives kind: "durable-object" services from worker.durableObjectNamespaces, and DurableObjectNamespace.local handles the subscribe side. The failure is upstream of it: the host worker's real instance never starts, so nothing ever publishes.
Regression window / what changed
Between beta.61 and beta.63, LocalWorkerProvider's serve path was rewritten (beta.61 src/Cloudflare/Workers/LocalWorkerProvider.ts ~L207-227 → beta.63 serveWith ~L232-280 + restartWorker + dropServeState):
- beta.61: start the new workerd first, then
Effect.forkDetach(Scope.close(previous)).
- beta.63:
Scope.close(previous) first (commented: "Both runtimes use the same registry key. Close the old scope first so its unregister finalizer cannot delete the replacement registration."), then Scope.fork + runtime.start, serialized per worker id via a new semaphore, with latestServes/localRuntimeState.workerRestarts powering sibling-triggered restarts (restartWorker explicitly no-ops when the worker hasn't served yet, assuming "the pending first serve will already observe the updated state").
Hypothesis (labeled as such)
The interrupt/re-serve flow around first startup loses host workers: their first serve gets interrupted (Changes detected, interrupting existing instance — e.g. the sibling Container resource updating), and nothing re-serves them afterwards — restartWorker no-ops (no latestServes entry yet, since the first serve never completed), and unlike consumer workers nothing else ever pokes them again. Net result: proxy up, real instance absent, no registry entry, every cross-script DO binding 503s. Workers that subscribe to something (queues, cross-script bindings) happen to survive, which matches the observed 3-registered/2-missing split exactly.
I couldn't pin the exact interleaving from outside — happy to run instrumented builds or provide more traces. Related: #856 (another beta.63 local-dev issue from the same setup — the capnweb RPC layer dropping Redacted in provider results; both currently block the container pattern locally, #856 patchable, this one not).
Symptom
Under
alchemy dev(2.0.0-beta.63), a worker binding a Durable Object from another worker viascriptNamegets persistent 503s:Setup: the documented container pattern — an async consumer worker + a small Effect worker hosting
Cloudflare.DurableObject+Cloudflare.Container(Containers.layer), bound cross-script:Deployed stages work. Local dev worked on 2.0.0-beta.61 and is broken on 2.0.0-beta.63 (beta.62 untested here; it already contains the relevant rewrite, see below).
Live forensics (three parallel
alchemy devstacks: app, eval, opt)~/.local/state/alchemy/registry/) contains entries only for the three subscriber workers (app-website-dev,eval-worker-dev,optrun-worker-dev). The two Effect host workers (eval-host-dev,opt-host-dev) have no entry at all — henceRegistryProxy.worker's resolution finds nothing and returns the 503.--debug-port=127.0.0.1:0— exactly the three registered workers (Runtime.startpasses{"debug-port": "127.0.0.1:0"}unconditionally and itscontext.start(ports)performs theregistry.write). The host workers exist only as their stable-port proxy instances; no real (debug-port) instance is running for them.[host-worker] Changes detected, interrupting existing instanceand the resource applies reportupdated— but a[host-worker] Started in …msline never appears (the sibling API workers log[worker] Started in 3023msnormally). The hosts'runtime.startnever completes.The publish machinery itself is complete and correct in
@distilled.cloud/cloudflare-runtime0.13.5:RegistryProxyLivederiveskind: "durable-object"services fromworker.durableObjectNamespaces, andDurableObjectNamespace.localhandles the subscribe side. The failure is upstream of it: the host worker's real instance never starts, so nothing ever publishes.Regression window / what changed
Between beta.61 and beta.63,
LocalWorkerProvider's serve path was rewritten (beta.61src/Cloudflare/Workers/LocalWorkerProvider.ts~L207-227 → beta.63serveWith~L232-280 +restartWorker+dropServeState):Effect.forkDetach(Scope.close(previous)).Scope.close(previous)first (commented: "Both runtimes use the same registry key. Close the old scope first so its unregister finalizer cannot delete the replacement registration."), thenScope.fork+runtime.start, serialized per worker id via a new semaphore, withlatestServes/localRuntimeState.workerRestartspowering sibling-triggered restarts (restartWorkerexplicitly no-ops when the worker hasn't served yet, assuming "the pending first serve will already observe the updated state").Hypothesis (labeled as such)
The interrupt/re-serve flow around first startup loses host workers: their first serve gets interrupted (
Changes detected, interrupting existing instance— e.g. the sibling Container resource updating), and nothing re-serves them afterwards —restartWorkerno-ops (nolatestServesentry yet, since the first serve never completed), and unlike consumer workers nothing else ever pokes them again. Net result: proxy up, real instance absent, no registry entry, every cross-script DO binding 503s. Workers that subscribe to something (queues, cross-script bindings) happen to survive, which matches the observed 3-registered/2-missing split exactly.I couldn't pin the exact interleaving from outside — happy to run instrumented builds or provide more traces. Related: #856 (another beta.63 local-dev issue from the same setup — the capnweb RPC layer dropping
Redactedin provider results; both currently block the container pattern locally, #856 patchable, this one not).