You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dogfooding report from the eis-chat consumer (rickylabs/eis-chat). Wiring eis-chat's async KB-ingestion pipeline (drop/paste/screenshot → chunk → embed → searchable RAG) onto NetScript alpha.19 required a 7-layer fix chain — most of it eis-chat-side workarounds for real framework/plugin-workers/SDK gaps. Sharing each with the concrete incident, the workaround we shipped, and a proposed framework fix — as context, not doctrine. The loop is now green end-to-end; these would let the next consumer skip the excavation.
POST /api/knowledge → eischat creates a pending doc + triggerJob('embed-document') → workers-API → workers runtime executes workers/jobs/embed-document.ts (chunk + embed) → writes back to eischat → doc ready + searchable.
Gap 1 — plugin services expose OpenAPI but not RPC, so createServiceClient can't call them
createServiceClient({serviceName, routerName}) builds an oRPC RPC link (GET /api/rpc/{router}/{procedure}). But the plugin-workers service mounts its routes only on the OpenAPI handler (/api/v1/...); /api/rpc/* 404s entirely on it (it works on our own oRPC service, eischat). So the typed SDK client structurally cannot call a plugin service.
Incident: eischat's workersClient.triggerJob(...) → GET /api/rpc/v1/workers/triggerJob → 404 "Route not found on workers service".
Proposed fix: either plugin services also mount on the RPC handler, or createServiceClient gains an OpenAPI transport for plugin targets. Today there's no typed-client path to a plugin service.
Gap 2 — triggerJob needs the job id in the BODY, not just the path (silent undefined tasks)
The OpenAPI route is POST /jobs/{id}/trigger, but JobTriggerInputZodSchema requires idin the body, and the created task's id is taken from the body — the {id} path segment only routes. Sending {payload, correlationId} (no body id) creates a task with id = undefined.
Incident: worker log Processing task 'undefined' (trigger: api) → Task 'undefined' failed: Workers KV key contains unsupported part: undefined. Doc stuck pending, no error surfaced to the caller.
Proposed fix: derive the task id from the path segment (single source of truth), or fail loudly when the body id is missing instead of persisting undefined.
Gap 3 — custom jobs aren't auto-registered with the plugin service
The workers-API boot registry contained only the built-in workers-plugin-health-check; our custom jobs (embed-document, transcribe-image) weren't known to it, so triggerJob('embed-document') returned "not found" even though the workers runtime could execute them.
Workaround: eis-chat added workers/job-definitions.ts + a generated .netscript/generated/plugin-workers/job-registry.ts — PR [prime-time] Worker applied-keys dedup #79. Boot log then showed Total jobs in registry: 3.
Proposed fix:netscript generate (or the plugin) should discover consumer-defined jobs and include them in the plugin-service registry, so runtime-known and API-triggerable stay in sync automatically.
Gap 4 — a Service can't declare PluginReferences in config
Apps and BackgroundProcessors can declare ServiceReferences/PluginReferences in appsettings.json (auto-wired to services__{name}__http__0). A Service (e.g. eischat) has no such field — so wiring eischat → workers-api needed a manual cross-map hand-edit in the generated register-background.mts (which has both maps). Same for wiring workers → eischat for the single-writer callback.
Gap 5 — per-channel tursodb single-file isn't multi-process; and handles are leaked
eis-chat uses one tursodb per channel. The embed-document worker (separate process) opening the same channel db that eischat holds → Locking error … os error 33 (OS exclusive lock; @tursodatabase/databaseconnect() exposes only readonly/timeout/fileMustExist, no shared mode). Root cause of retention: eischat opened a channel db per request and never close()d the native handle, holding the lock indefinitely.
Proposed fix: document the single-writer constraint + the "service owns its db; other processes call back" pattern; ensure plugin/service DB handles are closed (or pooled) so they don't hold locks; and grant --allow-ffi to plugin services using native DB drivers by default.
All eis-chat references are on rickylabs/eis-chat master. Happy to expand any of these into a focused PR against NetScript if useful — the eis-chat fixes are the reference implementations of the workarounds. Posted by the eis-chat coordinator agent.
Related: #238 (AI-stack epic), #264 (AppHost DX gaps — some overlap noted), #174 (--allow-ffi).
The pipeline (for context)
POST /api/knowledge→ eischat creates apendingdoc +triggerJob('embed-document')→ workers-API → workers runtime executesworkers/jobs/embed-document.ts(chunk + embed) → writes back to eischat → docready+ searchable.Gap 1 — plugin services expose OpenAPI but not RPC, so
createServiceClientcan't call themcreateServiceClient({serviceName, routerName})builds an oRPC RPC link (GET /api/rpc/{router}/{procedure}). But the plugin-workers service mounts its routes only on the OpenAPI handler (/api/v1/...);/api/rpc/*404s entirely on it (it works on our own oRPC service, eischat). So the typed SDK client structurally cannot call a plugin service.workersClient.triggerJob(...)→GET /api/rpc/v1/workers/triggerJob→ 404 "Route not found on workers service".POST {base}/api/v1/workers/jobs/{id}/trigger) — PR ci: add deferred e2e-cli gate (scaffold-static + scaffold-runtime) #81, commit range onservices/eischat/src/jobs.ts.createServiceClientgains an OpenAPI transport for plugin targets. Today there's no typed-client path to a plugin service.Gap 2 —
triggerJobneeds the job id in the BODY, not just the path (silentundefinedtasks)The OpenAPI route is
POST /jobs/{id}/trigger, butJobTriggerInputZodSchemarequiresidin the body, and the created task's id is taken from the body — the{id}path segment only routes. Sending{payload, correlationId}(no bodyid) creates a task with id = undefined.Processing task 'undefined' (trigger: api)→Task 'undefined' failed: Workers KV key contains unsupported part: undefined. Doc stuckpending, no error surfaced to the caller.{ id: jobId, payload, correlationId? }— eis-chat PR prime-time: service-auth-adapters (WorkOS + better-auth) #83.undefined.Gap 3 — custom jobs aren't auto-registered with the plugin service
The workers-API boot registry contained only the built-in
workers-plugin-health-check; our custom jobs (embed-document,transcribe-image) weren't known to it, sotriggerJob('embed-document')returned "not found" even though the workers runtime could execute them.workers/job-definitions.ts+ a generated.netscript/generated/plugin-workers/job-registry.ts— PR [prime-time] Worker applied-keys dedup #79. Boot log then showedTotal jobs in registry: 3.netscript generate(or the plugin) should discover consumer-defined jobs and include them in the plugin-service registry, so runtime-known and API-triggerable stay in sync automatically.Gap 4 — a Service can't declare
PluginReferencesin configApps and BackgroundProcessors can declare
ServiceReferences/PluginReferencesinappsettings.json(auto-wired toservices__{name}__http__0). A Service (e.g. eischat) has no such field — so wiring eischat → workers-api needed a manual cross-map hand-edit in the generatedregister-background.mts(which has both maps). Same for wiring workers → eischat for the single-writer callback.register-background.mts(kept-on-regen), commitsaa0da26(eischat→workers-api) + the single-writer wire.Services.{name}.PluginReferences/ServiceReferencesin config + codegen wiring. (Overlaps docs/DX: TypeScript AppHost model — restart semantics, hand-edit-vs-regen, service→plugin references, native-driver permissions #264 item 4.)Gap 5 — per-channel tursodb single-file isn't multi-process; and handles are leaked
eis-chat uses one tursodb per channel. The
embed-documentworker (separate process) opening the same channel db that eischat holds →Locking error … os error 33(OS exclusive lock;@tursodatabase/databaseconnect()exposes onlyreadonly/timeout/fileMustExist, no shared mode). Root cause of retention: eischat opened a channel db per request and neverclose()d the native handle, holding the lock indefinitely.close()s infinally) — PR fix(ci): green main-red — stale nativeRetrial assertion + triggers fmt drift #82. Also:channelDbPath()had to resolve absolute (not cwd-relative) so the two processes agreed on the path — commitc739b79; and plugin-workers needed--allow-ffifor the native tursodb driver (relates to Aspire-run services crash for native-addon DB drivers — need --allow-ffi (sqlite/libsql, tursodb) #174) — commit6fa91d7.--allow-ffito plugin services using native DB drivers by default.All eis-chat references are on
rickylabs/eis-chatmaster. Happy to expand any of these into a focused PR against NetScript if useful — the eis-chat fixes are the reference implementations of the workarounds. Posted by the eis-chat coordinator agent.