| name | testing-multi-worker-dev |
| description | How to run and test kody's multi-worker local dev (origin kody worker + kody-platform + kody-runtime + kody-jobs secondary configs), including known wrangler multi-config pitfalls (secondary worker env-name suffix, remote AI binding, .env secrets not propagated) and how to verify runtime-worker forwarding. |
Testing multi-worker local dev (kody + kody-platform + kody-runtime + kody-jobs)
Basics
- Node 26 required:
export PATH="$HOME/.nvm/versions/node/v26.7.0/bin:$PATH".
- To start or reuse the local app, run
npm run dev:ensure. It probes origin
/health on 3742–3751, prints App running at http://localhost:<port> and
exits 0 when a server is already up, replaces a stale kody/workerd leftover
that is listening but not serving, then starts npm run dev and waits until
/health is actually ok. Do not inventory Cursor terminal files or curl 3742
as a substitute.
- Run interactive
npm run dev in tmux when you need the CLI shortcuts; it
starts the client watcher, the mock Cloudflare API worker, and
wrangler dev --local with the origin config, the committed kody-jobs
config, and generated secondary configs for kody-runtime and kody-platform
in one miniflare. Default port 3742; the CLI picks the next free port if
taken.
- Local dev uses
--env production (CLOUDFLARE_ENV defaults to production in
wrangler-env.ts).
- Migrate + seed login:
npm run migrate:local then
node tools/seed-test-data.ts --local → kody@example.com / ilikecode
(admin) and jane@example.com / ilikecode.
- Healthchecks: origin
GET /health → {"ok":true,...}; platform worker serves
GET /__platform/health, runtime worker serves GET /__runtime/health, and
jobs worker serves GET /health. Those sibling paths 404 on the origin port.
Known wrangler multi-config pitfalls (handled by a generated dev config)
Wrangler applies --local, --var, and .env-derived secrets only to the
PRIMARY config, registers each worker under <name>-<env>, and treats a
secondary config's ai binding as always-remote (dev fails to boot with "Failed
to start the remote proxy session"; "remote": false is NOT enough).
wrangler-env.ts therefore never passes the committed runtime or platform
configs to wrangler dev directly: tools/local-runtime-dev-config.ts and
tools/local-platform-dev-config.ts generate
wrangler-local-dev.generated.json next to each committed config (gitignored)
on each dev start. Those files pin the secondary registered names to
kody-runtime and kody-platform, drop each secondary ai binding, rewrite
Durable Object migrations for local replay (tools/local-dev-migrations.ts:
transfers become new_sqlite_classes, create-then-delete pairs such as
PackageServiceInstance are elided), and inject APP_BASE_URL,
COOKIE_SECRET, SECRET_STORE_KEY, and WRANGLER_IS_LOCAL_DEV from the dev
process env. Runtime still rewrites any remaining script_name: "kody" refs to
the primary's dev name (kody-<env>). If runtime-owned paths 503 with
Worker "kody-runtime" not found or a secondary worker 500s on missing vars,
inspect those generated files first.
If npm run dev fails with
Cannot apply deleted_classes migration to non-existent class PackageServiceInstance,
the generated runtime config still has the production transfer+delete chain.
Wrangler's local sqlite-class map ignores transferred_classes, so the later
deleted_classes has nothing to delete. Confirm localizeMigrations ran and
that the generated file's top-level migrations (not only
env.production.migrations) has no PackageServiceInstance delete.
Jobs-worker Durable Object pitfall (boot failure)
With the jobs-worker extraction (ADR 0016), npm run dev may fail with
service core:user:kody-production: Uncaught TypeError: Class extends value undefined is not a constructor or null
(in miniflare's createDurableObjectWrapper) and "The Workers runtime failed to
start". Root cause: local dev (--env production) inherits the TOP-LEVEL
migrations chain in packages/worker/wrangler.jsonc, which still nets out
JobManager as a live DO class on the main script (v6 adds
JobManager+JobRunner, v9 only deletes JobRunner) — but JobManager is not
exported from packages/worker/src/index.ts (it lives in
packages/jobs-worker, which uses a transferred_classes migration from
kody-production). Miniflare replays the full chain fresh and tries to wrap the
missing class. The fix is a migration chain without the v6/v9 pair (matching
preview/test); if the error reappears, check that the top-level chain in
packages/worker/wrangler.jsonc does not create classes the main script does
not export.
Verifying forwarding
- Unauthenticated
GET /@<user>/packages/<anything> on the main port: 302 →
/login proves the runtime lane handled it (broken binding gives 503
instead).
POST /@<user>/api/package-invocations/x/y without bearer → 401
{"ok":false,"error":{"code":"unauthorized"}} from the runtime worker.
- Binding status line in dev output:
env.RUNTIME_WORKER (...) Worker local [connected] vs [not connected].
- Beware: logged-in package-app requests redirect to the real
https://kody.run
(production PACKAGE_APP_BASE_URL) — don't follow the handoff into prod.