fix(plugin-workers): a compiled job that does not reach the runtime registry fails at startup - #976
Conversation
…egistry now fails at startup
Three of four agents building on beta.11 hit the same wall: a job compiled into
.netscript/generated/plugin-workers/job-registry.ts that the worker runtime never
registered, surfacing as oRPC NOT_FOUND at triggerJob time — in one case after the
caller had already committed its own writes.
The registry was loaded on a silently-best-effort path that three of the four
workers entrypoints got wrong:
- bin/combined.ts resolved `../../<path>` against its own module URL, landing in
<root>/plugins/ instead of <root>/ — one directory short, so it never found the
file;
- bin/worker.ts and bin/scheduler.ts never loaded the registry at all;
- loadGeneratedJobRegistry mapped every miss — absent file, wrong exports, wrong
cwd — to `{}`, and registerStaticJobDefinitions returned early on empty, so all
of them were indistinguishable from "this project has no jobs".
Only workers-api resolved correctly, which made the whole feature depend on one
resource starting in the right directory and winning the race against the first
trigger.
resolveGeneratedJobRegistryUrl is now the only resolver, loading reports absent vs
loaded and throws when a registry exists but is unusable, and registration is
verified by reading every declared id back out of the runtime registry. The
generated runtime glue stops carrying its own copy of the loader.
The regression guard is a source scan asserting no entrypoint resolves the registry
path itself; it fails against the pre-fix bin/combined.ts and passes after.
Closes #951
|
[PHASE: IMPL] One slice landed: the generated workers job registry now has one resolver, and a compiled job that does not reach the runtime registry is a startup failure rather than a Slices
Landed as one commit: the loader signature change and its consumers are not independently shippable, and splitting would leave Root cause
Gate evidence
The regression guard is a source scan asserting no workers entrypoint resolves the registry path itself. Verified failing against the pre-fix Next
|
Summary
A job compiled into
.netscript/generated/plugin-workers/job-registry.tscould fail to reach the running worker runtime, surfacing as oRPCNOT_FOUNDattriggerJobtime — in one report after the caller had already committed its own writes. This makes the generated registry load through a single resolver, and turns "registry exists but did not reach the runtime registry" into a startup failure instead of silence.Scope
plugins/workersRoot cause — wider than the issue as filed
The issue has no comments, so the mechanism was re-derived from source. The component the issue names was the one that worked.
workers-api(services/src/main.ts) resolved the registry correctly viaprojectFileUrl. Three of the four workers entrypoints did not:bin/combined.tsresolved one directory short of the project root. It builtnew URL('../../' + WORKERS_JOB_REGISTRY_PATH, import.meta.url). From<root>/plugins/workers/bin/, two../reach<root>/plugins/, not<root>/. Verified by evaluation rather than inspection:bin/worker.tsandbin/scheduler.tsnever loaded the registry at all. Both callstartWorkerProcess()/startSchedulerProcess()with no options, andregisterStaticJobDefinitions(registry, undefined)returned immediately.Every miss was indistinguishable from "this project has no jobs."
loadGeneratedJobRegistrymappedDeno.errors.NotFoundto{}and returneddefinitions: undefinedfor an unrecognized module shape;registerStaticJobDefinitionsopened withif (!definitions?.size) return;. Absent file, wrong path, wrong cwd and malformed module all produced the same observable — a running worker with the health-check job and nothing else, and no log line anywhere.So whether a user's job was dispatchable depended on
workers-apistarting in the right directory and winning the race against the first trigger. That is the "returned after the parent record had already been written" shape in the report, and it explains all three symptoms from one mechanism — including "handler present, dispatcher still reporting 'not found in registry'":WorkerPoolsetsfallbackToDynamicImport: true, so the handler always resolved fromworkers/jobs/; it was the definition that was missing.What changed
resolveGeneratedJobRegistryUrl()insrc/runtime/generated-jobs.tsis now the only place the registry path becomes a URL.absent(no compiled registry — legitimate, stays non-fatal) orloaded, and throwsGeneratedJobRegistryErrorwhen a registry exists but exposes no usable definitions or fails to import.registerGeneratedJobRegistryre-reads every declared id out of the runtime registry after writing and throws naming the missing ids. Counting successful writes would not catch a write that reports success but does not land — which is the failure users actually saw.describeGeneratedJobRegistryemits one startup line naming the resolved path, and the cwd when absent. An off-by-one path is now visible in seconds.startWorkerProcess/startSchedulerProcess/startCombinedProcessresolve, load, register and verify when the caller supplies nodefinitions.bin/worker.tsandbin/scheduler.tsare unchanged and fixed by this.runtime.stub.tsemittedworkers/runtime.tscarrying its own inline copy of stat/import/instanceof Maplogic into every scaffolded project — a second implementation is how the two drifted. It is now a call tostartCombinedProcess().Regression guard
The existing checks looked just past this.
services/src/generated-jobs_test.tsasserted thatregisterGeneratedJobDefinitions"tolerates a missing generated registry" — the silent degrade was encoded as the desired contract — and it built the registry URL itself, so it never exercised the resolution that was broken.tests/cli/registry-compiler-golden_test.tslocks the emitted registry byte-for-byte; all three reporters got past it, because the file on disk was correct every time.plugins/workers/tests/runtime/generated-jobs_test.tsadds 8 guards, the load-bearing one being a source scan asserting no workers entrypoint resolves the registry path itself. Run against the pre-fixbin/combined.ts:and against this branch:
ok. The tolerates-silence test is replaced by one assertingabsentis reported, plus one asserting a compiled job that does not register now rejects. All wired intodeno task test; no new task needed.Validation
deno task fmt:checkdeno task lintdeno task checkdeno test plugins/workers packages/plugin-workers-coredeno test .../tests/runtime/generated-jobs_test.ts .../services/src/generated-jobs_test.tsdeno task arch:checkFAIL=0(warnings all pre-existing)deno task quality:scanrun-deno-doc-lint.ts --root plugins/workersmain, 0 missing JSDocNot run:
deno task e2e:cli run scaffold.runtime. The archetype matrix marks runtime/Aspire validation required for Archetype 5, and this PR changes plugin scaffold output (runtime.stub.ts), so the release-gate class applies. It needs a live Aspire + container graph that this session does not have. Flagging rather than silently skipping — it should run before merge. Note it would not have caught the original defect: its workers probe exercisesworkers-api, the one path that resolved correctly.Public surface
Additive on
@netscript/plugin-workers/runtime:resolveGeneratedJobRegistryUrl,registerGeneratedJobRegistry,describeGeneratedJobRegistry,GeneratedJobRegistryError,GeneratedJobRegistryStatus,StaticJobDefinitionRegistrar(was private; it is why doc-lint improved). Two widenings:GeneratedWorkersJobRegistrygainsstatus+url(existing fields unchanged, so{ definitions, registry }still destructures), andregisterStaticJobDefinitionsreturns a count instead ofvoid. The only in-repo consumer of the changed shape wasbin/combined.ts.Deliberately not changed
WORKERS_API_URLis declared as a literal string, not an Aspire service reference (src/aspire/workers-contribution.tsdeclareEnv). This is the likely mechanism behind the "workers missingServiceReferences" symptom reported in the same issue. Different file, different fix, different blast radius — worth its own issue rather than folding into a registry fix.WorkersAspireContributionregistersworkers-combinedalongside bothworkers-schedulerandworkers-worker, so the scheduler and worker each run twice. Noted, untouched.absentstays non-fatal. A project that has not run the compiler yet is legitimate; making it fatal would break every such workspace.workers/runtime.ts. That copy usesprojectFileUrland resolves correctly, so they are not broken — but they do not gain the loud startup check until the glue is regenerated.Harness
.llm/runs/fix-workers-generated-job-registry-load--fix-951/drift.mdD3 rather than left implicit.Drift / Debt
ServiceReferences), filed not folded.No new debt entries. The run removes an implicit one: a duplicated loader with divergent path resolution and no failure signal.