Skip to content

plugin-workers + SDK: 5 correctness/DX gaps surfaced wiring eis-chat async KB ingestion (OpenAPI-only routes, triggerJob body-id, job auto-registration, Service PluginReferences, per-channel tursodb single-writer) #279

Description

@rickylabs

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.

Related: #238 (AI-stack epic), #264 (AppHost DX gaps — some overlap noted), #174 (--allow-ffi).

The pipeline (for context)

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".
  • Workaround: eis-chat call the OpenAPI route directly (POST {base}/api/v1/workers/jobs/{id}/trigger) — PR ci: add deferred e2e-cli gate (scaffold-static + scaffold-runtime) #81, commit range on services/eischat/src/jobs.ts.
  • 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 id in 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.
  • Workaround: send { id: jobId, payload, correlationId? } — eis-chat PR prime-time: service-auth-adapters (WorkOS + better-auth) #83.
  • 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/database connect() 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.


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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions