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/healthon 3742–3751, printsApp running at http://localhost:<port>and exits 0 when a server is already up, waits for a stale kody/workerd leftover that is listening but not serving before replacing it, then startsnpm run devand waits until/healthis actually ok. Do not inventory Cursor terminal files or curl 3742 as a substitute. - Run interactive
npm run devin tmux when you need the CLI shortcuts; it starts the mock Cloudflare API worker, then Vite (@pitlane/dev+@cloudflare/vite-plugin) so origin SSR and the client hydrate in one workerd graph. Vite writespackages/worker/wrangler-local-dev.generated.jsonso originenvgetsWRANGLER_IS_LOCAL_DEVand mockCLOUDFLARE_API_*vars (the Cloudflare Vite plugin does not map process env onto Worker bindings). Jobs and highlight join as Vite auxiliary workers in every serve, includingCLOUDFLARE_ENV=test. Generated platform and runtime configs join only outside the test env. Default port 3742; the CLI picks the next free port if taken. - Local dev uses
--env production(CLOUDFLARE_ENV defaults to production inwrangler-env.ts). - Migrate + seed login:
npm run migrate:localthennode tools/seed-test-data.ts --local→kody@example.com/ilikecode(admin) andjane@example.com/ilikecode. - Healthchecks: origin
GET /health→{"ok":true,...}; platform worker servesGET /__platform/health, runtime worker servesGET /__runtime/health, and jobs worker servesGET /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).
Origin Vite serve uses the same pattern for the primary config:
tools/local-origin-dev-config.ts writes
packages/worker/wrangler-local-dev.generated.json and injects
WRANGLER_IS_LOCAL_DEV, mock CLOUDFLARE_API_*, and APP_BASE_URL into
vars. Worker secrets stay in packages/worker/.env / .dev.vars.
wrangler-env.ts still 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 or npm run runtime:build / startup-bundle dry-run fails with
Cannot apply deleted_classes migration to non-existent class PackageServiceInstance,
the config still has the production transfer+delete chain. Wrangler 4.131+
applies that local sqlite-class map on deploy --dry-run as well as dev.
wrangler-env.ts and tools/check-worker-startup-bundles.ts write
wrangler-dry-run.generated.json with localizeMigrations for those bundle
checks. wrangler check startup ignores --config and loads cwd
wrangler.jsonc, so check-worker-startup-time.ts snapshots a localized config
into a temp directory. Confirm localizeMigrations ran and that the generated
file's top-level migrations (not only env.production.migrations) has no
PackageServiceInstance delete. For npm run dev, confirm
wrangler-local-dev.generated.json instead.
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 →/loginproves the runtime lane handled it (broken binding gives 503 instead). POST /@<user>/api/package-invocations/x/ywithout 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(productionPACKAGE_APP_BASE_URL) — don't follow the handoff into prod.